@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.
- package/README.md +91 -2
- package/dist/agent-prompt.d.ts +16 -0
- package/dist/agent-prompt.d.ts.map +1 -0
- package/dist/avatar.d.ts +5 -0
- package/dist/avatar.d.ts.map +1 -0
- package/dist/badge.d.ts +11 -0
- package/dist/badge.d.ts.map +1 -0
- package/dist/button.d.ts +22 -0
- package/dist/button.d.ts.map +1 -0
- package/dist/callout.d.ts +12 -0
- package/dist/callout.d.ts.map +1 -0
- package/dist/card.d.ts +8 -0
- package/dist/card.d.ts.map +1 -0
- package/dist/checkbox.d.ts +3 -0
- package/dist/checkbox.d.ts.map +1 -0
- package/dist/choice-dialog.d.ts +34 -0
- package/dist/choice-dialog.d.ts.map +1 -0
- package/dist/code-block.d.ts +11 -0
- package/dist/code-block.d.ts.map +1 -0
- package/dist/data-table.d.ts +100 -0
- package/dist/data-table.d.ts.map +1 -0
- package/dist/dialog.d.ts +19 -0
- package/dist/dialog.d.ts.map +1 -0
- package/dist/dropdown-menu.d.ts +13 -0
- package/dist/dropdown-menu.d.ts.map +1 -0
- package/dist/empty-state.d.ts +11 -0
- package/dist/empty-state.d.ts.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1649 -0
- package/dist/index.js.map +1 -0
- package/dist/input.d.ts +21 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/kbd.d.ts +4 -0
- package/dist/kbd.d.ts.map +1 -0
- package/dist/label.d.ts +3 -0
- package/dist/label.d.ts.map +1 -0
- package/dist/radio-group.d.ts +35 -0
- package/dist/radio-group.d.ts.map +1 -0
- package/dist/segmented.d.ts +22 -0
- package/dist/segmented.d.ts.map +1 -0
- package/dist/select.d.ts +9 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/separator.d.ts +3 -0
- package/dist/separator.d.ts.map +1 -0
- package/dist/skeleton.d.ts +3 -0
- package/dist/skeleton.d.ts.map +1 -0
- package/dist/spinner.d.ts +4 -0
- package/dist/spinner.d.ts.map +1 -0
- package/dist/table-empty-state.d.ts +15 -0
- package/dist/table-empty-state.d.ts.map +1 -0
- package/dist/table-surface.d.ts +38 -0
- package/dist/table-surface.d.ts.map +1 -0
- package/dist/table.d.ts +15 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/tooltip.d.ts +6 -0
- package/dist/tooltip.d.ts.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +73 -4
- package/src/agent-prompt.tsx +60 -0
- package/src/avatar.tsx +46 -0
- package/src/badge.tsx +54 -0
- package/src/button.tsx +126 -0
- package/src/callout.tsx +74 -0
- package/src/card.tsx +61 -0
- package/src/checkbox.tsx +32 -0
- package/src/choice-dialog.tsx +110 -0
- package/src/code-block.tsx +53 -0
- package/src/data-table.tsx +749 -0
- package/src/dialog.tsx +123 -0
- package/src/dropdown-menu.tsx +103 -0
- package/src/empty-state.tsx +35 -0
- package/src/index.ts +26 -0
- package/src/input.tsx +195 -0
- package/src/kbd.tsx +16 -0
- package/src/label.tsx +19 -0
- package/src/radio-group.tsx +134 -0
- package/src/segmented.tsx +76 -0
- package/src/select.tsx +91 -0
- package/src/separator.tsx +22 -0
- package/src/skeleton.tsx +12 -0
- package/src/spinner.tsx +13 -0
- package/src/table-empty-state.tsx +51 -0
- package/src/table-surface.tsx +122 -0
- package/src/table.tsx +91 -0
- package/src/tooltip.tsx +32 -0
- package/src/utils.ts +7 -0
- 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
|
+
}
|