@jinaan/uikit 0.0.0-stage → 0.1.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.
Files changed (89) hide show
  1. package/README.md +91 -2
  2. package/dist/agent-prompt.d.ts +16 -0
  3. package/dist/agent-prompt.d.ts.map +1 -0
  4. package/dist/avatar.d.ts +5 -0
  5. package/dist/avatar.d.ts.map +1 -0
  6. package/dist/badge.d.ts +11 -0
  7. package/dist/badge.d.ts.map +1 -0
  8. package/dist/button.d.ts +22 -0
  9. package/dist/button.d.ts.map +1 -0
  10. package/dist/callout.d.ts +12 -0
  11. package/dist/callout.d.ts.map +1 -0
  12. package/dist/card.d.ts +8 -0
  13. package/dist/card.d.ts.map +1 -0
  14. package/dist/checkbox.d.ts +3 -0
  15. package/dist/checkbox.d.ts.map +1 -0
  16. package/dist/choice-dialog.d.ts +34 -0
  17. package/dist/choice-dialog.d.ts.map +1 -0
  18. package/dist/code-block.d.ts +11 -0
  19. package/dist/code-block.d.ts.map +1 -0
  20. package/dist/data-table.d.ts +100 -0
  21. package/dist/data-table.d.ts.map +1 -0
  22. package/dist/dialog.d.ts +19 -0
  23. package/dist/dialog.d.ts.map +1 -0
  24. package/dist/dropdown-menu.d.ts +13 -0
  25. package/dist/dropdown-menu.d.ts.map +1 -0
  26. package/dist/empty-state.d.ts +11 -0
  27. package/dist/empty-state.d.ts.map +1 -0
  28. package/dist/index.d.ts +27 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +1649 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/input.d.ts +21 -0
  33. package/dist/input.d.ts.map +1 -0
  34. package/dist/kbd.d.ts +4 -0
  35. package/dist/kbd.d.ts.map +1 -0
  36. package/dist/label.d.ts +3 -0
  37. package/dist/label.d.ts.map +1 -0
  38. package/dist/radio-group.d.ts +35 -0
  39. package/dist/radio-group.d.ts.map +1 -0
  40. package/dist/segmented.d.ts +22 -0
  41. package/dist/segmented.d.ts.map +1 -0
  42. package/dist/select.d.ts +9 -0
  43. package/dist/select.d.ts.map +1 -0
  44. package/dist/separator.d.ts +3 -0
  45. package/dist/separator.d.ts.map +1 -0
  46. package/dist/skeleton.d.ts +3 -0
  47. package/dist/skeleton.d.ts.map +1 -0
  48. package/dist/spinner.d.ts +4 -0
  49. package/dist/spinner.d.ts.map +1 -0
  50. package/dist/table-empty-state.d.ts +15 -0
  51. package/dist/table-empty-state.d.ts.map +1 -0
  52. package/dist/table-surface.d.ts +38 -0
  53. package/dist/table-surface.d.ts.map +1 -0
  54. package/dist/table.d.ts +15 -0
  55. package/dist/table.d.ts.map +1 -0
  56. package/dist/tooltip.d.ts +6 -0
  57. package/dist/tooltip.d.ts.map +1 -0
  58. package/dist/utils.d.ts +4 -0
  59. package/dist/utils.d.ts.map +1 -0
  60. package/package.json +73 -4
  61. package/src/agent-prompt.tsx +60 -0
  62. package/src/avatar.tsx +46 -0
  63. package/src/badge.tsx +54 -0
  64. package/src/button.tsx +126 -0
  65. package/src/callout.tsx +74 -0
  66. package/src/card.tsx +61 -0
  67. package/src/checkbox.tsx +32 -0
  68. package/src/choice-dialog.tsx +110 -0
  69. package/src/code-block.tsx +53 -0
  70. package/src/data-table.tsx +749 -0
  71. package/src/dialog.tsx +123 -0
  72. package/src/dropdown-menu.tsx +103 -0
  73. package/src/empty-state.tsx +35 -0
  74. package/src/index.ts +26 -0
  75. package/src/input.tsx +195 -0
  76. package/src/kbd.tsx +16 -0
  77. package/src/label.tsx +19 -0
  78. package/src/radio-group.tsx +134 -0
  79. package/src/segmented.tsx +76 -0
  80. package/src/select.tsx +91 -0
  81. package/src/separator.tsx +22 -0
  82. package/src/skeleton.tsx +12 -0
  83. package/src/spinner.tsx +13 -0
  84. package/src/table-empty-state.tsx +51 -0
  85. package/src/table-surface.tsx +122 -0
  86. package/src/table.tsx +91 -0
  87. package/src/tooltip.tsx +32 -0
  88. package/src/utils.ts +7 -0
  89. package/styles.css +371 -0
package/src/dialog.tsx ADDED
@@ -0,0 +1,123 @@
1
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
2
+ import { X } from "@phosphor-icons/react";
3
+ import {
4
+ forwardRef,
5
+ type ComponentPropsWithoutRef,
6
+ type ElementRef,
7
+ type HTMLAttributes,
8
+ } from "react";
9
+
10
+ import { cn } from "./utils";
11
+
12
+ export const Dialog = DialogPrimitive.Root;
13
+ export const DialogTrigger = DialogPrimitive.Trigger;
14
+ export const DialogClose = DialogPrimitive.Close;
15
+
16
+ export const DialogContent = forwardRef<
17
+ ElementRef<typeof DialogPrimitive.Content>,
18
+ ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
19
+ >(({ className, children, ...props }, ref) => (
20
+ <DialogPrimitive.Portal>
21
+ <DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-overlay backdrop-blur-[1px] data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0" />
22
+ <DialogPrimitive.Content
23
+ ref={ref}
24
+ className={cn(
25
+ "fixed left-1/2 top-1/2 z-50 flex max-h-[min(85vh,44rem)] w-full max-w-[22.4rem] -translate-x-1/2 -translate-y-1/2 flex-col gap-0 overflow-hidden",
26
+ "rounded-2xl border border-[var(--dialog-shell-border)] bg-[var(--dialog-shell-bg)] pb-3 shadow-[var(--dialog-shell-shadow)] sm:pb-4",
27
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
28
+ "data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
29
+ "data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
30
+ "data-[state=open]:slide-in-from-top-2 duration-200 ease-out",
31
+ "focus:outline-none",
32
+ className,
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <DialogPrimitive.Close className="absolute right-3.5 top-3.5 z-10 rounded-sm text-subtle transition-colors hover:text-fg focus-visible:outline-none">
38
+ <X className="size-4" />
39
+ <span className="sr-only">Close</span>
40
+ </DialogPrimitive.Close>
41
+ </DialogPrimitive.Content>
42
+ </DialogPrimitive.Portal>
43
+ ));
44
+ DialogContent.displayName = "DialogContent";
45
+
46
+ /**
47
+ * Elevated inner card inside the dialog shell. Flush to the shell's left, top
48
+ * and right edges (the shell clips its top corners); the footer stays below on
49
+ * the shell so the two surfaces read as distinct layers.
50
+ */
51
+ export function DialogPanel({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
52
+ return (
53
+ <div
54
+ data-slot="panel"
55
+ className={cn(
56
+ "flex min-h-0 flex-1 flex-col overflow-hidden rounded-b-2xl border-x-0 border-t-0 border-b border-[var(--dialog-panel-border)] bg-[var(--dialog-panel-bg)] shadow-[var(--dialog-panel-shadow)]",
57
+ className,
58
+ )}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ /** Scrollable region inside `DialogPanel`; keeps the footer pinned. */
65
+ export function DialogBody({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
66
+ return (
67
+ <div
68
+ data-slot="body"
69
+ className={cn("min-h-0 flex-1 overflow-y-auto px-5 py-4", className)}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ export function DialogHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
76
+ return (
77
+ <div
78
+ data-slot="header"
79
+ className={cn(
80
+ "flex shrink-0 flex-col gap-1.5 border-b border-[var(--dialog-panel-border)] px-5 py-4",
81
+ className,
82
+ )}
83
+ {...props}
84
+ />
85
+ );
86
+ }
87
+
88
+ export function DialogFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
89
+ return (
90
+ <div
91
+ data-slot="footer"
92
+ className={cn(
93
+ "flex shrink-0 flex-col-reverse gap-2 px-3 pt-3 sm:flex-row sm:justify-end sm:px-4 sm:pt-3.5",
94
+ className,
95
+ )}
96
+ {...props}
97
+ />
98
+ );
99
+ }
100
+
101
+ export const DialogTitle = forwardRef<
102
+ ElementRef<typeof DialogPrimitive.Title>,
103
+ ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
104
+ >(({ className, ...props }, ref) => (
105
+ <DialogPrimitive.Title
106
+ ref={ref}
107
+ className={cn("text-base font-semibold text-fg", className)}
108
+ {...props}
109
+ />
110
+ ));
111
+ DialogTitle.displayName = "DialogTitle";
112
+
113
+ export const DialogDescription = forwardRef<
114
+ ElementRef<typeof DialogPrimitive.Description>,
115
+ ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
116
+ >(({ className, ...props }, ref) => (
117
+ <DialogPrimitive.Description
118
+ ref={ref}
119
+ className={cn("text-sm text-muted", className)}
120
+ {...props}
121
+ />
122
+ ));
123
+ DialogDescription.displayName = "DialogDescription";
@@ -0,0 +1,103 @@
1
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
2
+ import { Check } from "@phosphor-icons/react";
3
+ import {
4
+ forwardRef,
5
+ type ComponentPropsWithoutRef,
6
+ type ElementRef,
7
+ } from "react";
8
+
9
+ import { cn } from "./utils";
10
+
11
+ export const DropdownMenu = DropdownMenuPrimitive.Root;
12
+ export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
13
+ export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14
+ export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
15
+
16
+ export const DropdownMenuContent = forwardRef<
17
+ ElementRef<typeof DropdownMenuPrimitive.Content>,
18
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
19
+ >(({ className, sideOffset = 6, ...props }, ref) => (
20
+ <DropdownMenuPrimitive.Portal>
21
+ <DropdownMenuPrimitive.Content
22
+ ref={ref}
23
+ sideOffset={sideOffset}
24
+ className={cn(
25
+ "z-50 min-w-48 overflow-hidden rounded-lg border border-line bg-surface p-1 shadow-md",
26
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
27
+ "data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
28
+ "data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
29
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
30
+ "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
31
+ "duration-150 ease-out",
32
+ className,
33
+ )}
34
+ {...props}
35
+ />
36
+ </DropdownMenuPrimitive.Portal>
37
+ ));
38
+ DropdownMenuContent.displayName = "DropdownMenuContent";
39
+
40
+ export const DropdownMenuItem = forwardRef<
41
+ ElementRef<typeof DropdownMenuPrimitive.Item>,
42
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { inset?: boolean }
43
+ >(({ className, inset, ...props }, ref) => (
44
+ <DropdownMenuPrimitive.Item
45
+ ref={ref}
46
+ className={cn(
47
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-fg outline-none transition-colors",
48
+ "focus:bg-elevated data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
49
+ "[&_svg]:size-4 [&_svg]:text-muted",
50
+ inset && "pl-8",
51
+ className,
52
+ )}
53
+ {...props}
54
+ />
55
+ ));
56
+ DropdownMenuItem.displayName = "DropdownMenuItem";
57
+
58
+ export const DropdownMenuCheckboxItem = forwardRef<
59
+ ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
60
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
61
+ >(({ className, children, checked, ...props }, ref) => (
62
+ <DropdownMenuPrimitive.CheckboxItem
63
+ ref={ref}
64
+ className={cn(
65
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-fg outline-none transition-colors focus:bg-elevated",
66
+ className,
67
+ )}
68
+ checked={checked}
69
+ {...props}
70
+ >
71
+ <span className="absolute left-2 flex size-4 items-center justify-center">
72
+ <DropdownMenuPrimitive.ItemIndicator>
73
+ <Check className="size-3.5" />
74
+ </DropdownMenuPrimitive.ItemIndicator>
75
+ </span>
76
+ {children}
77
+ </DropdownMenuPrimitive.CheckboxItem>
78
+ ));
79
+ DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
80
+
81
+ export const DropdownMenuLabel = forwardRef<
82
+ ElementRef<typeof DropdownMenuPrimitive.Label>,
83
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>
84
+ >(({ className, ...props }, ref) => (
85
+ <DropdownMenuPrimitive.Label
86
+ ref={ref}
87
+ className={cn("px-2 py-1.5 text-xs font-medium text-subtle", className)}
88
+ {...props}
89
+ />
90
+ ));
91
+ DropdownMenuLabel.displayName = "DropdownMenuLabel";
92
+
93
+ export const DropdownMenuSeparator = forwardRef<
94
+ ElementRef<typeof DropdownMenuPrimitive.Separator>,
95
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
96
+ >(({ className, ...props }, ref) => (
97
+ <DropdownMenuPrimitive.Separator
98
+ ref={ref}
99
+ className={cn("-mx-1 my-1 h-px bg-line", className)}
100
+ {...props}
101
+ />
102
+ ));
103
+ DropdownMenuSeparator.displayName = "DropdownMenuSeparator";
@@ -0,0 +1,35 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { cn } from "./utils";
4
+
5
+ interface EmptyStateProps {
6
+ icon?: ReactNode;
7
+ title: string;
8
+ description?: string;
9
+ action?: ReactNode;
10
+ className?: string;
11
+ }
12
+
13
+ export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
14
+ return (
15
+ <div
16
+ className={cn(
17
+ "flex flex-col items-center justify-center gap-4 rounded-card border border-line bg-surface px-6 py-16 text-center",
18
+ className,
19
+ )}
20
+ >
21
+ {icon ? (
22
+ <div className="flex size-11 items-center justify-center rounded-xl border border-line bg-elevated text-subtle [&_svg]:size-6">
23
+ {icon}
24
+ </div>
25
+ ) : null}
26
+ <div className="space-y-1">
27
+ <p className="text-sm font-medium text-fg">{title}</p>
28
+ {description ? (
29
+ <p className="mx-auto max-w-sm text-sm text-muted">{description}</p>
30
+ ) : null}
31
+ </div>
32
+ {action}
33
+ </div>
34
+ );
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ export * from "./agent-prompt";
2
+ export * from "./avatar";
3
+ export * from "./badge";
4
+ export * from "./button";
5
+ export * from "./callout";
6
+ export * from "./card";
7
+ export * from "./checkbox";
8
+ export * from "./choice-dialog";
9
+ export * from "./code-block";
10
+ export * from "./data-table";
11
+ export * from "./dialog";
12
+ export * from "./dropdown-menu";
13
+ export * from "./empty-state";
14
+ export * from "./input";
15
+ export * from "./kbd";
16
+ export * from "./label";
17
+ export * from "./radio-group";
18
+ export * from "./segmented";
19
+ export * from "./select";
20
+ export * from "./separator";
21
+ export * from "./skeleton";
22
+ export * from "./spinner";
23
+ export * from "./table";
24
+ export * from "./table-empty-state";
25
+ export * from "./table-surface";
26
+ export * from "./tooltip";
package/src/input.tsx ADDED
@@ -0,0 +1,195 @@
1
+ import { X } from "@phosphor-icons/react";
2
+ import { forwardRef, useId, type InputHTMLAttributes, type ReactNode } from "react";
3
+
4
+ import { cn } from "./utils";
5
+
6
+ type InputSize = "sm" | "md" | "lg" | "xl";
7
+
8
+ const HEIGHT: Record<InputSize, string> = {
9
+ sm: "h-[var(--control-h-sm)] text-sm",
10
+ md: "h-[var(--control-h-md)] text-sm",
11
+ lg: "h-[var(--control-h-lg)] text-sm",
12
+ xl: "h-[var(--control-h-xl)] rounded-[var(--control-radius-xl)] text-base",
13
+ };
14
+
15
+ const PAD_X: Record<InputSize, string> = {
16
+ sm: "px-[var(--control-pad-x-sm)]",
17
+ md: "px-[var(--control-pad-x-md)]",
18
+ lg: "px-[var(--control-pad-x-lg)]",
19
+ xl: "px-[var(--control-pad-x-xl)]",
20
+ };
21
+
22
+ const LEAD_INSET: Record<InputSize, string> = {
23
+ sm: "left-[var(--control-pad-x-sm)]",
24
+ md: "left-[var(--control-pad-x-md)]",
25
+ lg: "left-[var(--control-pad-x-lg)]",
26
+ xl: "left-[var(--control-pad-x-xl)]",
27
+ };
28
+
29
+ const TRAIL_INSET: Record<InputSize, string> = {
30
+ sm: "right-[var(--control-pad-x-sm)]",
31
+ md: "right-[var(--control-pad-x-md)]",
32
+ lg: "right-[var(--control-pad-x-lg)]",
33
+ xl: "right-[var(--control-pad-x-xl)]",
34
+ };
35
+
36
+ const ICON: Record<InputSize, string> = {
37
+ sm: "size-[var(--control-icon-sm)]",
38
+ md: "size-[var(--control-icon-md)]",
39
+ lg: "size-[var(--control-icon-lg)]",
40
+ xl: "size-[var(--control-icon-xl)]",
41
+ };
42
+
43
+ export interface InputProps
44
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
45
+ /** Control height preset. Defaults to `md`. */
46
+ size?: InputSize;
47
+ /** Optional icon rendered inside the field, before the text. */
48
+ leadingIcon?: ReactNode;
49
+ /** Optional icon rendered inside the field, after the text. */
50
+ trailingIcon?: ReactNode;
51
+ /** Shows a real clear button when the field has a value. */
52
+ onClear?: () => void;
53
+ /** Error styling; also pairs with `helperText` for the message. */
54
+ error?: boolean;
55
+ /** Helper or error message shown below the field (wired to aria-describedby). */
56
+ helperText?: string;
57
+ /** Wrapper class when icons or a helper message render a container. */
58
+ containerClassName?: string;
59
+ }
60
+
61
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
62
+ (
63
+ {
64
+ className,
65
+ containerClassName,
66
+ type = "text",
67
+ size = "md",
68
+ leadingIcon,
69
+ trailingIcon,
70
+ onClear,
71
+ value,
72
+ defaultValue,
73
+ error = false,
74
+ helperText,
75
+ id,
76
+ "aria-describedby": ariaDescribedBy,
77
+ disabled,
78
+ readOnly,
79
+ ...props
80
+ },
81
+ ref,
82
+ ) => {
83
+ const autoId = useId();
84
+ const inputId = id ?? autoId;
85
+ const hasMessage = Boolean(helperText);
86
+ const showClear = Boolean(onClear && (value !== undefined ? String(value).length > 0 : defaultValue !== undefined ? String(defaultValue).length > 0 : false));
87
+ const messageId = hasMessage ? `${inputId}-description` : undefined;
88
+ const describedBy = [ariaDescribedBy, messageId].filter(Boolean).join(" ") || undefined;
89
+
90
+ const field = (
91
+ <input
92
+ ref={ref}
93
+ id={inputId}
94
+ type={type}
95
+ disabled={disabled}
96
+ readOnly={readOnly}
97
+ aria-invalid={error || undefined}
98
+ aria-describedby={describedBy}
99
+ style={
100
+ leadingIcon || trailingIcon || showClear
101
+ ? {
102
+ paddingInlineStart: leadingIcon
103
+ ? `calc(var(--control-pad-x-${size}) + var(--control-icon-${size}) + var(--control-gap))`
104
+ : undefined,
105
+ paddingInlineEnd:
106
+ trailingIcon || showClear
107
+ ? `calc(var(--control-pad-x-${size}) + var(--control-icon-${size}) + var(--control-gap))`
108
+ : undefined,
109
+ }
110
+ : undefined
111
+ }
112
+ className={cn(
113
+ "flex w-full rounded-[var(--control-radius)] border bg-[var(--input-bg)] text-[var(--input-fg)] transition-colors",
114
+ HEIGHT[size],
115
+ PAD_X[size],
116
+ "placeholder:text-[var(--input-placeholder)]",
117
+ "hover:border-[var(--input-border-hover)]",
118
+ "focus-visible:outline-none focus-visible:border-[var(--input-border-focus)] focus-visible:ring-2 focus-visible:ring-focus/30",
119
+ error &&
120
+ "border-error hover:border-error focus-visible:border-error focus-visible:ring-error/30",
121
+ !error && "border-[var(--input-border)]",
122
+ disabled &&
123
+ "cursor-not-allowed bg-[var(--input-bg-disabled)] opacity-60 hover:border-[var(--input-border)]",
124
+ readOnly && "cursor-default focus-visible:ring-0",
125
+ className,
126
+ )}
127
+ value={value}
128
+ defaultValue={defaultValue}
129
+ {...props}
130
+ />
131
+ );
132
+
133
+ // Bare input keeps the historical API when no compositional features are used.
134
+ if (!leadingIcon && !trailingIcon && !showClear && !hasMessage) {
135
+ return field;
136
+ }
137
+
138
+ return (
139
+ <div className={cn("w-full", containerClassName)}>
140
+ <div className="relative">
141
+ {leadingIcon ? (
142
+ <span
143
+ className={cn(
144
+ "pointer-events-none absolute inset-y-0 flex items-center text-[var(--input-placeholder)]",
145
+ LEAD_INSET[size],
146
+ )}
147
+ aria-hidden
148
+ >
149
+ <span className={cn("inline-flex items-center justify-center", ICON[size])}>
150
+ {leadingIcon}
151
+ </span>
152
+ </span>
153
+ ) : null}
154
+ {field}
155
+ {showClear ? (
156
+ <button
157
+ type="button"
158
+ aria-label="Clear search"
159
+ onClick={onClear}
160
+ className={cn(
161
+ "absolute inset-y-0 flex items-center text-[var(--input-placeholder)] transition-colors hover:text-[var(--input-fg)]",
162
+ TRAIL_INSET[size],
163
+ )}
164
+ >
165
+ <span className={cn("inline-flex items-center justify-center", ICON[size])}>
166
+ <X />
167
+ </span>
168
+ </button>
169
+ ) : trailingIcon ? (
170
+ <span
171
+ className={cn(
172
+ "pointer-events-none absolute inset-y-0 flex items-center text-[var(--input-placeholder)]",
173
+ TRAIL_INSET[size],
174
+ )}
175
+ aria-hidden
176
+ >
177
+ <span className={cn("inline-flex items-center justify-center", ICON[size])}>
178
+ {trailingIcon}
179
+ </span>
180
+ </span>
181
+ ) : null}
182
+ </div>
183
+ {hasMessage ? (
184
+ <p
185
+ id={messageId}
186
+ className={cn("mt-1.5 text-xs", error ? "text-error" : "text-muted")}
187
+ >
188
+ {helperText}
189
+ </p>
190
+ ) : null}
191
+ </div>
192
+ );
193
+ },
194
+ );
195
+ Input.displayName = "Input";
package/src/kbd.tsx ADDED
@@ -0,0 +1,16 @@
1
+ import type { HTMLAttributes } from "react";
2
+
3
+ import { cn } from "./utils";
4
+
5
+ /** Inline keyboard hint chip. */
6
+ export function Kbd({ className, ...props }: HTMLAttributes<HTMLElement>) {
7
+ return (
8
+ <kbd
9
+ className={cn(
10
+ "inline-flex h-5 min-w-5 items-center justify-center rounded border border-line bg-elevated px-1.5 font-mono text-[11px] font-medium text-subtle",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
package/src/label.tsx ADDED
@@ -0,0 +1,19 @@
1
+ import * as LabelPrimitive from "@radix-ui/react-label";
2
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
3
+
4
+ import { cn } from "./utils";
5
+
6
+ export const Label = forwardRef<
7
+ ElementRef<typeof LabelPrimitive.Root>,
8
+ ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
9
+ >(({ className, ...props }, ref) => (
10
+ <LabelPrimitive.Root
11
+ ref={ref}
12
+ className={cn(
13
+ "text-sm font-medium text-fg leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ ));
19
+ Label.displayName = "Label";
@@ -0,0 +1,134 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useId,
5
+ type HTMLAttributes,
6
+ type ReactNode,
7
+ } from "react";
8
+
9
+ import { Badge } from "./badge";
10
+ import { cn } from "./utils";
11
+
12
+ interface RadioGroupContextValue {
13
+ name: string;
14
+ value?: string;
15
+ onChange?: (value: string) => void;
16
+ disabled?: boolean;
17
+ }
18
+
19
+ const RadioGroupContext = createContext<RadioGroupContextValue | null>(null);
20
+
21
+ export interface RadioGroupProps
22
+ extends Omit<HTMLAttributes<HTMLFieldSetElement>, "onChange"> {
23
+ /** Accessible group name; also the shared `name` for the native radios. */
24
+ label: string;
25
+ /** Selected value (controlled). */
26
+ value?: string;
27
+ /** Called with the newly selected value. */
28
+ onChange?: (value: string) => void;
29
+ /** Disables every option in the group. */
30
+ disabled?: boolean;
31
+ /** Shared `name` for the native inputs; generated when omitted. */
32
+ name?: string;
33
+ }
34
+
35
+ /**
36
+ * Vertical group of `RadioCard` options. Built on native `<input type="radio">`
37
+ * inside a `<fieldset>`, so arrow-key navigation, labels and forms work without
38
+ * a dependency. Controlled via `value` / `onChange`.
39
+ */
40
+ export function RadioGroup({
41
+ label,
42
+ value,
43
+ onChange,
44
+ disabled = false,
45
+ name,
46
+ className,
47
+ children,
48
+ ...props
49
+ }: RadioGroupProps) {
50
+ const generated = useId();
51
+ const groupName = name ?? generated;
52
+
53
+ return (
54
+ <RadioGroupContext.Provider value={{ name: groupName, value, onChange, disabled }}>
55
+ <fieldset className={cn("flex flex-col gap-6", className)} {...props}>
56
+ <legend className="sr-only">{label}</legend>
57
+ {children}
58
+ </fieldset>
59
+ </RadioGroupContext.Provider>
60
+ );
61
+ }
62
+
63
+ export interface RadioCardProps {
64
+ /** Value reported when this option is selected. */
65
+ value: string;
66
+ /** Primary label. */
67
+ title: ReactNode;
68
+ /** Supporting copy shown under the title. */
69
+ description?: ReactNode;
70
+ /** Optional pill rendered next to the title (e.g. "Easiest"). */
71
+ badge?: ReactNode;
72
+ /** Disables just this option. */
73
+ disabled?: boolean;
74
+ className?: string;
75
+ }
76
+
77
+ /** A single selectable option: radio indicator, title, optional badge + copy. */
78
+ export function RadioCard({
79
+ value,
80
+ title,
81
+ description,
82
+ badge,
83
+ disabled = false,
84
+ className,
85
+ }: RadioCardProps) {
86
+ const ctx = useContext(RadioGroupContext);
87
+ const describedById = useId();
88
+ if (!ctx) {
89
+ throw new Error("RadioCard must be rendered inside a RadioGroup");
90
+ }
91
+
92
+ const isDisabled = disabled || ctx.disabled;
93
+ const isChecked = ctx.value === value;
94
+
95
+ return (
96
+ <label
97
+ className={cn(
98
+ "group relative flex items-start gap-3",
99
+ isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
100
+ className,
101
+ )}
102
+ >
103
+ <input
104
+ type="radio"
105
+ name={ctx.name}
106
+ value={value}
107
+ checked={isChecked}
108
+ disabled={isDisabled}
109
+ onChange={() => ctx.onChange?.(value)}
110
+ aria-describedby={description ? describedById : undefined}
111
+ className="peer sr-only"
112
+ />
113
+ <span
114
+ aria-hidden
115
+ className={cn(
116
+ "mt-0.5 grid size-4.5 shrink-0 place-items-center rounded-full border-2 border-transparent bg-line-strong transition-colors",
117
+ "peer-checked:border-[5px] peer-checked:border-primary peer-checked:bg-primary-fg",
118
+ "peer-focus-visible:ring-2 peer-focus-visible:ring-focus/40 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-canvas",
119
+ )}
120
+ />
121
+ <span className="min-w-0 flex-1">
122
+ <span className="flex flex-wrap items-center gap-2">
123
+ <span className="text-base font-semibold text-fg">{title}</span>
124
+ {badge ? <Badge variant="primary">{badge}</Badge> : null}
125
+ </span>
126
+ {description ? (
127
+ <span id={describedById} className="mt-1 block text-sm text-muted">
128
+ {description}
129
+ </span>
130
+ ) : null}
131
+ </span>
132
+ </label>
133
+ );
134
+ }