@kinetixui/ui 0.17.0 → 0.19.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 +16 -1
- package/dist/index.js +455 -238
- package/package.json +2 -2
- package/src/components/audio-player.tsx +2 -2
- package/src/components/badge.tsx +1 -1
- package/src/components/banner.tsx +1 -1
- package/src/components/button.tsx +8 -8
- 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 +4 -0
- package/src/components/data-grid.tsx +276 -24
- 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/input-group.tsx +1 -1
- package/src/components/input-otp.tsx +2 -1
- package/src/components/input.tsx +2 -2
- package/src/components/json-viewer.tsx +12 -10
- 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/multi-select.tsx +14 -3
- package/src/components/native-select.tsx +2 -2
- 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 +2 -2
- package/src/components/skeleton.tsx +1 -1
- package/src/components/slider.tsx +2 -2
- 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/textarea.tsx +2 -2
- package/src/components/timeline.tsx +1 -1
- package/src/components/tooltip.tsx +1 -1
- package/src/components/virtual-list.tsx +2 -1
- package/src/components-a11y.test.tsx +4 -25
- package/src/components-behavior.test.tsx +1 -1
- package/src/components-keyboard.test.tsx +388 -34
- package/src/stories/AppBar.stories.tsx +1 -1
- package/src/stories/Checkbox.stories.tsx +1 -1
- package/src/stories/CircularProgress.stories.tsx +4 -4
- package/src/stories/DataGrid.stories.tsx +2 -1
- 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 +14 -0
|
@@ -59,17 +59,11 @@ const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
59
59
|
|
|
60
60
|
return (
|
|
61
61
|
<div ref={ref} className={cn("flex w-full flex-col gap-3 font-sans", className)} {...props}>
|
|
62
|
+
{/* A drag-and-drop surface, not a control: the Button inside is the one keyboard/AT target
|
|
63
|
+
(a role="button" containing a Button is nested-interactive). Clicking the surface stays a
|
|
64
|
+
mouse convenience. */}
|
|
62
65
|
<div
|
|
63
|
-
role="button"
|
|
64
|
-
tabIndex={disabled ? -1 : 0}
|
|
65
|
-
aria-disabled={disabled}
|
|
66
66
|
onClick={() => !disabled && inputRef.current?.click()}
|
|
67
|
-
onKeyDown={(e) => {
|
|
68
|
-
if (!disabled && (e.key === "Enter" || e.key === " ")) {
|
|
69
|
-
e.preventDefault();
|
|
70
|
-
inputRef.current?.click();
|
|
71
|
-
}
|
|
72
|
-
}}
|
|
73
67
|
onDragOver={(e) => {
|
|
74
68
|
e.preventDefault();
|
|
75
69
|
if (!disabled) setDragging(true);
|
|
@@ -81,9 +75,9 @@ const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
81
75
|
if (!disabled) pick(e.dataTransfer.files);
|
|
82
76
|
}}
|
|
83
77
|
className={cn(
|
|
84
|
-
"flex flex-col items-center gap-2 rounded-md border border-dashed border-input p-6 text-center
|
|
85
|
-
!disabled && "cursor-pointer hover:bg-accent/50
|
|
86
|
-
dragging && "border-
|
|
78
|
+
"flex flex-col items-center gap-2 rounded-md border border-dashed border-input p-6 text-center transition-colors",
|
|
79
|
+
!disabled && "cursor-pointer hover:bg-accent/50",
|
|
80
|
+
dragging && "border-action bg-accent",
|
|
87
81
|
disabled && "pointer-events-none opacity-disabled",
|
|
88
82
|
)}
|
|
89
83
|
>
|
|
@@ -108,6 +102,10 @@ const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
108
102
|
multiple={multiple}
|
|
109
103
|
accept={accept}
|
|
110
104
|
disabled={disabled}
|
|
105
|
+
// driven by the dropzone button above; a second focusable control inside a role=button
|
|
106
|
+
// (and an unlabeled one) is invalid, so keep it out of the tab order and the a11y tree
|
|
107
|
+
tabIndex={-1}
|
|
108
|
+
aria-hidden="true"
|
|
111
109
|
className="sr-only"
|
|
112
110
|
onChange={(e) => {
|
|
113
111
|
pick(e.target.files);
|
|
@@ -141,7 +139,7 @@ function FileUploadItem({ file, onRemove, onRetry }: FileUploadItemProps) {
|
|
|
141
139
|
return (
|
|
142
140
|
<li className={cn("flex items-center gap-2.5 rounded-md border p-2.5 text-body-sm", isError ? "border-destructive bg-destructive/5" : "border-input")}>
|
|
143
141
|
{isLoading ? (
|
|
144
|
-
<Loader2 className="size-4 shrink-0 animate-spin text-muted-foreground" aria-hidden />
|
|
142
|
+
<Loader2 className="size-4 shrink-0 animate-spin motion-reduce:animate-[spin_3s_linear_infinite] text-muted-foreground" aria-hidden />
|
|
145
143
|
) : isError ? (
|
|
146
144
|
<TriangleAlert className="size-4 shrink-0 text-destructive" aria-hidden />
|
|
147
145
|
) : (
|
|
@@ -152,7 +150,7 @@ function FileUploadItem({ file, onRemove, onRetry }: FileUploadItemProps) {
|
|
|
152
150
|
{isError && file.error ? <p className="text-destructive">{file.error}</p> : <p className="text-muted-foreground">{formatBytes(file.size)}</p>}
|
|
153
151
|
{isLoading && typeof file.progress === "number" && (
|
|
154
152
|
<div className="mt-1 h-1 w-full overflow-hidden rounded-full bg-muted">
|
|
155
|
-
<div className="h-full bg-
|
|
153
|
+
<div className="h-full bg-action transition-[width]" style={{ width: `${Math.max(0, Math.min(100, file.progress))}%` }} />
|
|
156
154
|
</div>
|
|
157
155
|
)}
|
|
158
156
|
</div>
|
|
@@ -160,7 +158,7 @@ function FileUploadItem({ file, onRemove, onRetry }: FileUploadItemProps) {
|
|
|
160
158
|
<button
|
|
161
159
|
type="button"
|
|
162
160
|
onClick={() => onRetry(file.id)}
|
|
163
|
-
className="shrink-0 text-label-md font-medium text-
|
|
161
|
+
className="shrink-0 text-label-md font-medium text-link underline-offset-2 outline-none hover:underline focus-visible:underline"
|
|
164
162
|
>
|
|
165
163
|
Retry
|
|
166
164
|
</button>
|
|
@@ -22,7 +22,7 @@ const InputGroup = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDiv
|
|
|
22
22
|
data-slot="input-group"
|
|
23
23
|
className={cn(
|
|
24
24
|
"flex w-full items-center overflow-hidden rounded-sm border border-input bg-background font-sans transition-colors",
|
|
25
|
-
"focus-within:border-
|
|
25
|
+
"focus-within:border-action focus-within:shadow-focus",
|
|
26
26
|
"has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:focus-within:shadow-focus-destructive",
|
|
27
27
|
"has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-disabled",
|
|
28
28
|
className,
|
|
@@ -35,7 +35,8 @@ const InputOTPSlot = React.forwardRef<
|
|
|
35
35
|
ref={ref}
|
|
36
36
|
className={cn(
|
|
37
37
|
"relative flex size-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
|
38
|
-
|
|
38
|
+
// ring is a box-shadow, which forced-colors mode strips; the outline survives it
|
|
39
|
+
isActive && "z-docked ring-1 ring-ring forced-colors:[outline:2px_solid] forced-colors:[outline-offset:-2px]",
|
|
39
40
|
className,
|
|
40
41
|
)}
|
|
41
42
|
{...props}
|
package/src/components/input.tsx
CHANGED
|
@@ -25,7 +25,7 @@ const inputVariants = cva(
|
|
|
25
25
|
"font-sans text-body-md text-foreground",
|
|
26
26
|
"placeholder:text-muted-foreground",
|
|
27
27
|
"outline-none transition-colors",
|
|
28
|
-
"focus-visible:border-
|
|
28
|
+
"focus-visible:border-action focus-visible:shadow-focus",
|
|
29
29
|
"disabled:cursor-not-allowed disabled:opacity-disabled",
|
|
30
30
|
"aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive aria-[invalid=true]:focus-visible:shadow-focus-destructive",
|
|
31
31
|
],
|
|
@@ -33,7 +33,7 @@ const inputVariants = cva(
|
|
|
33
33
|
variants: {
|
|
34
34
|
state: {
|
|
35
35
|
Default: "",
|
|
36
|
-
Focus: "border-
|
|
36
|
+
Focus: "border-action shadow-focus",
|
|
37
37
|
Error: "border-destructive",
|
|
38
38
|
Disabled: "opacity-disabled pointer-events-none",
|
|
39
39
|
},
|
|
@@ -136,16 +136,18 @@ function JsonNode({
|
|
|
136
136
|
</div>
|
|
137
137
|
{expanded && (
|
|
138
138
|
<>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
139
|
+
<div role="group">
|
|
140
|
+
{entries.map(([key, val], i) => (
|
|
141
|
+
<JsonNode
|
|
142
|
+
key={key}
|
|
143
|
+
name={isArray ? undefined : key}
|
|
144
|
+
value={val}
|
|
145
|
+
depth={depth + 1}
|
|
146
|
+
expandDepth={expandDepth}
|
|
147
|
+
isLast={i === entries.length - 1}
|
|
148
|
+
/>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
149
151
|
<div style={{ paddingLeft: depth * 16 }}>
|
|
150
152
|
<span>{closeBracket}</span>
|
|
151
153
|
{!isLast && <span className="text-muted-foreground">,</span>}
|
package/src/components/list.tsx
CHANGED
|
@@ -29,39 +29,53 @@ export interface ListItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>
|
|
|
29
29
|
const ListItem = React.forwardRef<HTMLDivElement, ListItemProps>(
|
|
30
30
|
({ className, leading, title, description, trailing, disabled, onSelect, ...props }, ref) => {
|
|
31
31
|
const interactive = !!onSelect && !disabled;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
43
|
-
e.preventDefault();
|
|
44
|
-
onSelect?.();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
: undefined
|
|
48
|
-
}
|
|
49
|
-
className={cn(
|
|
50
|
-
"flex items-center gap-3 px-3 py-3 font-sans outline-none",
|
|
51
|
-
// ring keeps the highlighted row perceptible where the accent fill is <3:1 (dark)
|
|
52
|
-
interactive &&
|
|
53
|
-
"cursor-pointer hover:bg-accent hover:ring-1 hover:ring-inset hover:ring-ring focus-visible:bg-accent focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
|
|
54
|
-
disabled && "pointer-events-none opacity-disabled",
|
|
55
|
-
className,
|
|
56
|
-
)}
|
|
57
|
-
{...props}
|
|
58
|
-
>
|
|
32
|
+
const rowClass = cn(
|
|
33
|
+
"flex items-center gap-3 px-3 py-3 font-sans outline-none",
|
|
34
|
+
// ring keeps the highlighted row perceptible where the accent fill is <3:1 (dark)
|
|
35
|
+
interactive &&
|
|
36
|
+
"cursor-pointer hover:bg-accent hover:ring-1 hover:ring-inset hover:ring-ring focus-visible:bg-accent focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
|
|
37
|
+
disabled && "pointer-events-none opacity-disabled",
|
|
38
|
+
className,
|
|
39
|
+
);
|
|
40
|
+
const content = (
|
|
41
|
+
<>
|
|
59
42
|
{leading && <div className="flex shrink-0 items-center justify-center">{leading}</div>}
|
|
60
43
|
<div className="min-w-0 flex-1">
|
|
61
44
|
<div className="truncate text-body-md text-foreground">{title}</div>
|
|
62
45
|
{description && <div className="truncate text-body-sm text-muted-foreground">{description}</div>}
|
|
63
46
|
</div>
|
|
64
47
|
{trailing && <div className="flex shrink-0 items-center gap-2">{trailing}</div>}
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!interactive) {
|
|
52
|
+
return (
|
|
53
|
+
<div ref={ref} role="listitem" aria-disabled={disabled} className={rowClass} {...props}>
|
|
54
|
+
{content}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A list's only valid children are listitems, so a pressable row is a listitem that
|
|
60
|
+
// contains the button — not a button standing in for the listitem.
|
|
61
|
+
return (
|
|
62
|
+
<div role="listitem">
|
|
63
|
+
<div
|
|
64
|
+
ref={ref}
|
|
65
|
+
role="button"
|
|
66
|
+
tabIndex={0}
|
|
67
|
+
onClick={onSelect}
|
|
68
|
+
onKeyDown={(e) => {
|
|
69
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
onSelect?.();
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
74
|
+
className={rowClass}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
{content}
|
|
78
|
+
</div>
|
|
65
79
|
</div>
|
|
66
80
|
);
|
|
67
81
|
},
|
|
@@ -163,7 +163,7 @@ function renderMarkdown(source: string): string {
|
|
|
163
163
|
* this ports cleanly to all four platforms instead.
|
|
164
164
|
*/
|
|
165
165
|
const MarkdownEditor = React.forwardRef<HTMLDivElement, MarkdownEditorProps>(
|
|
166
|
-
({ value, onChange, placeholder, rows = 10, className, ...props }, ref) => {
|
|
166
|
+
({ value, onChange, placeholder, rows = 10, className, "aria-label": ariaLabel = "Markdown", ...props }, ref) => {
|
|
167
167
|
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
168
168
|
const [showPreview, setShowPreview] = React.useState(false);
|
|
169
169
|
const pendingSelection = React.useRef<{ start: number; end: number } | null>(null);
|
|
@@ -236,11 +236,12 @@ const MarkdownEditor = React.forwardRef<HTMLDivElement, MarkdownEditorProps>(
|
|
|
236
236
|
onChange={(e) => onChange(e.target.value)}
|
|
237
237
|
placeholder={placeholder}
|
|
238
238
|
rows={rows}
|
|
239
|
+
aria-label={ariaLabel}
|
|
239
240
|
className="resize-none rounded-none border-0 font-mono text-[13px] focus-visible:border-transparent focus-visible:shadow-none"
|
|
240
241
|
/>
|
|
241
242
|
{showPreview && (
|
|
242
243
|
<div
|
|
243
|
-
className="overflow-auto p-3 text-sm [&_a]:text-
|
|
244
|
+
className="overflow-auto p-3 text-sm [&_a]:text-link [&_a]:underline [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground [&_code]:rounded-sm [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em] [&_h1]:text-xl [&_h1]:font-semibold [&_h2]:text-lg [&_h2]:font-semibold [&_h3]:font-semibold [&_li]:ml-5 [&_ol]:list-decimal [&_p]:mb-2 [&_pre]:overflow-auto [&_pre]:rounded-sm [&_pre]:bg-muted [&_pre]:p-2 [&_ul]:list-disc"
|
|
244
245
|
// Rendered from renderMarkdown above, which HTML-escapes all
|
|
245
246
|
// source text before inserting any markup — safe from
|
|
246
247
|
// injection, same trust boundary as any other self-generated
|
|
@@ -34,7 +34,7 @@ const MessageBubble = React.forwardRef<HTMLDivElement, MessageBubbleProps>(
|
|
|
34
34
|
<div
|
|
35
35
|
className={cn(
|
|
36
36
|
"min-w-0 rounded-2xl px-3.5 py-2 text-body-sm",
|
|
37
|
-
sent ? "bg-
|
|
37
|
+
sent ? "bg-action text-action-foreground" : "bg-muted text-foreground",
|
|
38
38
|
sent && grouped && "rounded-tr-md",
|
|
39
39
|
!sent && grouped && "rounded-tl-md",
|
|
40
40
|
)}
|
|
@@ -45,7 +45,7 @@ const MessageBubble = React.forwardRef<HTMLDivElement, MessageBubbleProps>(
|
|
|
45
45
|
<div className="flex items-center gap-1 px-1 text-label-sm text-muted-foreground">
|
|
46
46
|
{timestamp}
|
|
47
47
|
{sent && status && (
|
|
48
|
-
<span className={cn(status === "read" && "text-
|
|
48
|
+
<span className={cn(status === "read" && "text-action")}>
|
|
49
49
|
{status === "sent" ? <Check className="size-3.5" /> : <CheckCheck className="size-3.5" />}
|
|
50
50
|
</span>
|
|
51
51
|
)}
|
|
@@ -71,7 +71,7 @@ const TypingIndicator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HT
|
|
|
71
71
|
<span
|
|
72
72
|
key={i}
|
|
73
73
|
aria-hidden
|
|
74
|
-
className="size-1.5 animate-typing-dot rounded-full bg-muted-foreground"
|
|
74
|
+
className="size-1.5 animate-typing-dot motion-reduce:animate-none motion-reduce:opacity-70 rounded-full bg-muted-foreground"
|
|
75
75
|
style={{ animationDelay: `${i * 150}ms` }}
|
|
76
76
|
/>
|
|
77
77
|
))}
|
|
@@ -82,8 +82,8 @@ const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
|
|
|
82
82
|
}}
|
|
83
83
|
className={cn(
|
|
84
84
|
"flex min-h-11 w-full flex-wrap items-center gap-1.5 border border-input bg-background px-3 py-2 font-sans",
|
|
85
|
-
"outline-none focus-visible:border-
|
|
86
|
-
"aria-expanded:border-
|
|
85
|
+
"outline-none focus-visible:border-action focus-visible:shadow-focus",
|
|
86
|
+
"aria-expanded:border-action aria-expanded:shadow-focus",
|
|
87
87
|
className,
|
|
88
88
|
)}
|
|
89
89
|
{...props}
|
|
@@ -108,7 +108,18 @@ const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
|
|
|
108
108
|
}}
|
|
109
109
|
>
|
|
110
110
|
<Command shouldFilter={!creatable}>
|
|
111
|
-
<CommandInput
|
|
111
|
+
<CommandInput
|
|
112
|
+
placeholder="Search…"
|
|
113
|
+
value={query}
|
|
114
|
+
onValueChange={setQuery}
|
|
115
|
+
onKeyDown={(e) => {
|
|
116
|
+
// Backspace in an empty search field removes the last chip, as in every tag input
|
|
117
|
+
if (e.key === "Backspace" && query === "" && current.length > 0) {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
remove(current[current.length - 1]!);
|
|
120
|
+
}
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
112
123
|
<CommandList>
|
|
113
124
|
<CommandEmpty>
|
|
114
125
|
{canCreate ? (
|
|
@@ -18,7 +18,7 @@ const nativeSelectVariants = cva(
|
|
|
18
18
|
"flex w-full appearance-none border border-input bg-background px-3 py-3 pe-9",
|
|
19
19
|
"font-sans text-body-md text-foreground",
|
|
20
20
|
"outline-none transition-colors",
|
|
21
|
-
"focus-visible:border-
|
|
21
|
+
"focus-visible:border-action focus-visible:shadow-focus",
|
|
22
22
|
"disabled:cursor-not-allowed disabled:opacity-disabled",
|
|
23
23
|
"aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive aria-[invalid=true]:focus-visible:shadow-focus-destructive",
|
|
24
24
|
],
|
|
@@ -26,7 +26,7 @@ const nativeSelectVariants = cva(
|
|
|
26
26
|
variants: {
|
|
27
27
|
state: {
|
|
28
28
|
Default: "",
|
|
29
|
-
Focus: "border-
|
|
29
|
+
Focus: "border-action shadow-focus",
|
|
30
30
|
Error: "border-destructive",
|
|
31
31
|
Disabled: "opacity-disabled pointer-events-none",
|
|
32
32
|
},
|
|
@@ -56,7 +56,7 @@ const NotificationCenterContent = React.forwardRef<HTMLDivElement, NotificationC
|
|
|
56
56
|
<button
|
|
57
57
|
type="button"
|
|
58
58
|
onClick={onMarkAllRead}
|
|
59
|
-
className="text-label-md font-medium text-
|
|
59
|
+
className="text-label-md font-medium text-link outline-none hover:underline focus-visible:underline"
|
|
60
60
|
>
|
|
61
61
|
Mark all read
|
|
62
62
|
</button>
|
|
@@ -102,7 +102,7 @@ const NotificationItem = React.forwardRef<HTMLDivElement, NotificationItemProps>
|
|
|
102
102
|
)}
|
|
103
103
|
{...props}
|
|
104
104
|
>
|
|
105
|
-
<span className={cn("mt-1.5 size-2 shrink-0 rounded-full", unread ? "bg-
|
|
105
|
+
<span className={cn("mt-1.5 size-2 shrink-0 rounded-full", unread ? "bg-action" : "bg-transparent")} />
|
|
106
106
|
<div className="min-w-0 flex-1">
|
|
107
107
|
<p className={cn("truncate text-body-sm", unread ? "font-medium text-foreground" : "text-muted-foreground")}>
|
|
108
108
|
{title}
|
|
@@ -14,7 +14,7 @@ const Progress = React.forwardRef<
|
|
|
14
14
|
{...props}
|
|
15
15
|
>
|
|
16
16
|
<ProgressPrimitive.Indicator
|
|
17
|
-
className="size-full flex-1 bg-
|
|
17
|
+
className="size-full flex-1 bg-action transition-transform"
|
|
18
18
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
19
19
|
/>
|
|
20
20
|
</ProgressPrimitive.Root>
|
|
@@ -25,17 +25,17 @@ const RadioGroupItem = React.forwardRef<
|
|
|
25
25
|
<RadioGroupPrimitive.Item
|
|
26
26
|
ref={ref}
|
|
27
27
|
className={cn(
|
|
28
|
-
"aspect-square size-[18px] rounded-full border-2 border-input text-
|
|
28
|
+
"aspect-square size-[18px] rounded-full border-2 border-input text-action outline-none transition-colors",
|
|
29
29
|
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
30
30
|
"disabled:cursor-not-allowed disabled:opacity-disabled",
|
|
31
|
-
"data-[state=checked]:border-
|
|
31
|
+
"data-[state=checked]:border-action",
|
|
32
32
|
"aria-invalid:border-destructive",
|
|
33
33
|
className,
|
|
34
34
|
)}
|
|
35
35
|
{...props}
|
|
36
36
|
>
|
|
37
37
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
38
|
-
<Circle className="size-2.5 fill-
|
|
38
|
+
<Circle className="size-2.5 fill-action text-action" />
|
|
39
39
|
</RadioGroupPrimitive.Indicator>
|
|
40
40
|
</RadioGroupPrimitive.Item>
|
|
41
41
|
));
|
|
@@ -9,7 +9,7 @@ const ScrollArea = React.forwardRef<
|
|
|
9
9
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
|
10
10
|
>(({ className, children, ...props }, ref) => (
|
|
11
11
|
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
|
|
12
|
-
<ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
|
|
12
|
+
<ScrollAreaPrimitive.Viewport tabIndex={0} className="size-full rounded-[inherit] outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring">{children}</ScrollAreaPrimitive.Viewport>
|
|
13
13
|
<ScrollBar />
|
|
14
14
|
<ScrollAreaPrimitive.Corner />
|
|
15
15
|
</ScrollAreaPrimitive.Root>
|
|
@@ -25,8 +25,8 @@ const SelectTrigger = React.forwardRef<
|
|
|
25
25
|
"font-sans text-body-md text-foreground",
|
|
26
26
|
"data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1",
|
|
27
27
|
"outline-none transition-colors",
|
|
28
|
-
"focus:border-
|
|
29
|
-
"data-[state=open]:border-
|
|
28
|
+
"focus:border-action focus:shadow-focus",
|
|
29
|
+
"data-[state=open]:border-action",
|
|
30
30
|
"aria-[invalid=true]:border-destructive aria-[invalid=true]:focus:border-destructive aria-[invalid=true]:focus:shadow-focus-destructive",
|
|
31
31
|
"disabled:cursor-not-allowed disabled:opacity-disabled",
|
|
32
32
|
className,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { cn } from "../lib/utils";
|
|
4
4
|
|
|
5
5
|
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
6
|
-
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />;
|
|
6
|
+
return <div className={cn("animate-pulse rounded-md bg-muted motion-reduce:animate-none", className)} {...props} />;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export { Skeleton };
|
|
@@ -14,13 +14,13 @@ const Slider = React.forwardRef<
|
|
|
14
14
|
{...props}
|
|
15
15
|
>
|
|
16
16
|
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted">
|
|
17
|
-
<SliderPrimitive.Range className="absolute h-full bg-
|
|
17
|
+
<SliderPrimitive.Range className="absolute h-full bg-action" />
|
|
18
18
|
</SliderPrimitive.Track>
|
|
19
19
|
{/* the accessible name belongs on the thumb: that is the element with role="slider" */}
|
|
20
20
|
<SliderPrimitive.Thumb
|
|
21
21
|
aria-label={ariaLabel}
|
|
22
22
|
aria-labelledby={ariaLabelledBy}
|
|
23
|
-
className="block size-4 rounded-full border border-
|
|
23
|
+
className="block size-4 rounded-full border border-action/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-disabled"
|
|
24
24
|
/>
|
|
25
25
|
</SliderPrimitive.Root>
|
|
26
26
|
));
|
|
@@ -18,7 +18,7 @@ function Toaster({ theme = "system", ...props }: ToasterProps) {
|
|
|
18
18
|
toast:
|
|
19
19
|
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
|
20
20
|
description: "group-[.toast]:text-muted-foreground",
|
|
21
|
-
actionButton: "group-[.toast]:bg-
|
|
21
|
+
actionButton: "group-[.toast]:bg-action group-[.toast]:text-action-foreground",
|
|
22
22
|
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
|
23
23
|
},
|
|
24
24
|
}}
|
|
@@ -9,7 +9,7 @@ import { cn } from "../lib/utils";
|
|
|
9
9
|
* dependency; `size` sets the diameter, `variant` swaps the indicator
|
|
10
10
|
* colour for on-color surfaces.
|
|
11
11
|
*/
|
|
12
|
-
const spinnerVariants = cva("inline-block animate-spin rounded-full border-2 border-current border-t-transparent", {
|
|
12
|
+
const spinnerVariants = cva("inline-block animate-spin motion-reduce:animate-[spin_3s_linear_infinite] rounded-full border-2 border-current border-t-transparent", {
|
|
13
13
|
variants: {
|
|
14
14
|
size: {
|
|
15
15
|
sm: "size-4",
|
|
@@ -17,9 +17,9 @@ const spinnerVariants = cva("inline-block animate-spin rounded-full border-2 bor
|
|
|
17
17
|
lg: "size-8",
|
|
18
18
|
},
|
|
19
19
|
variant: {
|
|
20
|
-
default: "text-
|
|
20
|
+
default: "text-action",
|
|
21
21
|
muted: "text-muted-foreground",
|
|
22
|
-
onColor: "text-
|
|
22
|
+
onColor: "text-action-foreground",
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
defaultVariants: { size: "md", variant: "default" },
|
|
@@ -39,8 +39,8 @@ const Stepper = React.forwardRef<HTMLOListElement, StepperProps>(
|
|
|
39
39
|
<span
|
|
40
40
|
className={cn(
|
|
41
41
|
"flex size-7 shrink-0 items-center justify-center rounded-full text-label-md font-medium",
|
|
42
|
-
status === "complete" && "bg-
|
|
43
|
-
status === "current" && "border-2 border-
|
|
42
|
+
status === "complete" && "bg-action text-action-foreground",
|
|
43
|
+
status === "current" && "border-2 border-action text-action",
|
|
44
44
|
status === "upcoming" && "border border-input text-muted-foreground",
|
|
45
45
|
)}
|
|
46
46
|
>
|
|
@@ -52,7 +52,7 @@ const Stepper = React.forwardRef<HTMLOListElement, StepperProps>(
|
|
|
52
52
|
className={cn(
|
|
53
53
|
"bg-border",
|
|
54
54
|
vertical ? "my-1 min-h-6 w-px flex-1 self-stretch" : "mt-3.5 h-px flex-1",
|
|
55
|
-
status === "complete" && "bg-
|
|
55
|
+
status === "complete" && "bg-action",
|
|
56
56
|
)}
|
|
57
57
|
/>
|
|
58
58
|
)}
|
|
@@ -19,7 +19,7 @@ const Switch = React.forwardRef<
|
|
|
19
19
|
"peer inline-flex h-6 w-12 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors",
|
|
20
20
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
21
21
|
"disabled:cursor-not-allowed disabled:opacity-disabled",
|
|
22
|
-
"data-[state=checked]:bg-
|
|
22
|
+
"data-[state=checked]:bg-action data-[state=unchecked]:bg-tertiary",
|
|
23
23
|
className,
|
|
24
24
|
)}
|
|
25
25
|
{...props}
|
|
@@ -35,7 +35,7 @@ const TabBarItem = React.forwardRef<HTMLButtonElement, TabBarItemProps>(
|
|
|
35
35
|
aria-current={active ? "page" : undefined}
|
|
36
36
|
className={cn(
|
|
37
37
|
"relative flex flex-1 flex-col items-center justify-center gap-1 py-2 text-label-sm outline-none transition-colors",
|
|
38
|
-
active ? "text-
|
|
38
|
+
active ? "text-action" : "text-muted-foreground hover:text-foreground",
|
|
39
39
|
"focus-visible:bg-accent",
|
|
40
40
|
className,
|
|
41
41
|
)}
|
|
@@ -31,7 +31,7 @@ const TableOfContents = React.forwardRef<HTMLElement, TableOfContentsProps>(
|
|
|
31
31
|
style={{ paddingInlineStart: `${((item.level ?? 1) - 1) * 12 + 12}px` }}
|
|
32
32
|
className={cn(
|
|
33
33
|
"-ms-px border-s py-1.5 text-body-sm outline-none transition-colors",
|
|
34
|
-
isActive ? "border-
|
|
34
|
+
isActive ? "border-action font-medium text-action" : "border-transparent text-muted-foreground hover:text-foreground",
|
|
35
35
|
)}
|
|
36
36
|
>
|
|
37
37
|
{item.label}
|
|
@@ -17,7 +17,7 @@ const textareaVariants = cva(
|
|
|
17
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,
|
|
@@ -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);
|
|
@@ -10,37 +10,16 @@ import { afterEach, describe, expect, it } from "vitest";
|
|
|
10
10
|
* association. jsdom has no layout or stylesheet, so `color-contrast` (covered
|
|
11
11
|
* by `pnpm check:contrast`) and page-level rules are off.
|
|
12
12
|
*
|
|
13
|
-
* KNOWN lists violations that
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* entry) until it is deleted from the list, so the baseline only shrinks.
|
|
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.
|
|
17
16
|
*/
|
|
18
17
|
const stories = import.meta.glob("./stories/*.stories.tsx", { eager: true }) as Record<
|
|
19
18
|
string,
|
|
20
19
|
Record<string, unknown>
|
|
21
20
|
>;
|
|
22
21
|
|
|
23
|
-
const KNOWN = new Set<string>([
|
|
24
|
-
"Banner:landmark-no-duplicate-banner",
|
|
25
|
-
"Banner:landmark-unique",
|
|
26
|
-
"CircularProgress:aria-progressbar-name",
|
|
27
|
-
"ColorPicker:label",
|
|
28
|
-
"DiffViewer:aria-required-children",
|
|
29
|
-
"FileUpload:label",
|
|
30
|
-
"FileUpload:nested-interactive",
|
|
31
|
-
"InputOTP:label",
|
|
32
|
-
"JsonViewer:aria-required-parent",
|
|
33
|
-
"List:aria-required-children",
|
|
34
|
-
"List:button-name",
|
|
35
|
-
"MarkdownEditor:label",
|
|
36
|
-
"MultiSelect:aria-input-field-name",
|
|
37
|
-
"NativeSelect:select-name",
|
|
38
|
-
"NumberInput:label",
|
|
39
|
-
"Progress:aria-progressbar-name",
|
|
40
|
-
"Select:button-name",
|
|
41
|
-
"Slider:aria-input-field-name",
|
|
42
|
-
"ToggleGroup:button-name",
|
|
43
|
-
]);
|
|
22
|
+
const KNOWN = new Set<string>([]);
|
|
44
23
|
|
|
45
24
|
afterEach(cleanup);
|
|
46
25
|
|
|
@@ -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
|
|