@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAG7C,uEAAuE;AACvE,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,CAElD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jinaan/uikit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Reusable React and Tailwind UI components for Jinaan projects.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"*.css"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src",
|
|
13
|
+
"styles.css",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./styles.css": "./styles.css"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "vite --config vite.dev.config.ts",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"audit:theme": "node scripts/audit-theme.mjs",
|
|
30
|
+
"build": "pnpm typecheck && vite build && tsc -p tsconfig.build.json",
|
|
31
|
+
"prepublishOnly": "pnpm build"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=18",
|
|
35
|
+
"react-dom": ">=18",
|
|
36
|
+
"tailwindcss": ">=4"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@phosphor-icons/react": "^2.1.7",
|
|
40
|
+
"@radix-ui/react-avatar": "^1.1.2",
|
|
41
|
+
"@radix-ui/react-checkbox": "^1.1.4",
|
|
42
|
+
"@radix-ui/react-dialog": "^1.1.4",
|
|
43
|
+
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
|
44
|
+
"@radix-ui/react-label": "^2.1.1",
|
|
45
|
+
"@radix-ui/react-select": "^2.1.4",
|
|
46
|
+
"@radix-ui/react-separator": "^1.1.1",
|
|
47
|
+
"@radix-ui/react-slot": "^1.1.1",
|
|
48
|
+
"@radix-ui/react-tooltip": "^1.1.6",
|
|
49
|
+
"class-variance-authority": "^0.7.1",
|
|
50
|
+
"clsx": "^2.1.1",
|
|
51
|
+
"tailwind-merge": "^2.6.0",
|
|
52
|
+
"tw-animate-css": "^1.4.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
56
|
+
"@types/node": "^22.20.5",
|
|
57
|
+
"@types/react": "^19.0.2",
|
|
58
|
+
"@types/react-dom": "^19.0.2",
|
|
59
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
60
|
+
"jsdom": "^25.0.1",
|
|
61
|
+
"react": "^19.0.0",
|
|
62
|
+
"react-dom": "^19.0.0",
|
|
63
|
+
"tailwindcss": "^4.0.0",
|
|
64
|
+
"typescript": "~5.7.2",
|
|
65
|
+
"vite": "^6.0.5",
|
|
66
|
+
"vitest": "^2.1.8"
|
|
67
|
+
},
|
|
68
|
+
"publishConfig": {
|
|
69
|
+
"access": "public"
|
|
70
|
+
},
|
|
71
|
+
"engines": {
|
|
72
|
+
"node": ">=20",
|
|
73
|
+
"pnpm": ">=9"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Check, Copy, Robot, Terminal } from "@phosphor-icons/react";
|
|
2
|
+
import { useState, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
interface AgentPromptProps {
|
|
8
|
+
/** Command or prompt text; copy copies this verbatim. */
|
|
9
|
+
command: string;
|
|
10
|
+
/** Overlapping dark tag above the pill. */
|
|
11
|
+
tag?: ReactNode;
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The "Add Clerk auth to my app" prompt pill: a dark tag overlapping a
|
|
18
|
+
* rounded command field with a copy button.
|
|
19
|
+
*/
|
|
20
|
+
export function AgentPrompt({
|
|
21
|
+
command,
|
|
22
|
+
tag = "Build with agents",
|
|
23
|
+
icon,
|
|
24
|
+
className,
|
|
25
|
+
}: AgentPromptProps) {
|
|
26
|
+
const [copied, setCopied] = useState(false);
|
|
27
|
+
|
|
28
|
+
const copy = async () => {
|
|
29
|
+
try {
|
|
30
|
+
await navigator.clipboard.writeText(command);
|
|
31
|
+
setCopied(true);
|
|
32
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
33
|
+
} catch {
|
|
34
|
+
/* clipboard unavailable */
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={cn("relative inline-flex max-w-full", className)}>
|
|
40
|
+
<span className="absolute -top-2.5 left-4 z-10 inline-flex items-center gap-1.5 rounded-md bg-fg px-2 py-1 text-[11px] font-medium text-canvas shadow-sm">
|
|
41
|
+
{icon ?? <Robot className="size-3.5" weight="fill" />}
|
|
42
|
+
{tag}
|
|
43
|
+
</span>
|
|
44
|
+
<div className="flex w-full items-center gap-2 rounded-xl border border-line bg-surface py-2 pl-3 pr-2 shadow-sm">
|
|
45
|
+
<Terminal className="size-4 shrink-0 text-subtle" aria-hidden />
|
|
46
|
+
<span className="truncate font-mono text-sm text-fg">{command}</span>
|
|
47
|
+
<Button
|
|
48
|
+
variant="ghost"
|
|
49
|
+
size="sm"
|
|
50
|
+
iconOnly
|
|
51
|
+
aria-label="Copy prompt"
|
|
52
|
+
className="ml-auto"
|
|
53
|
+
onClick={copy}
|
|
54
|
+
>
|
|
55
|
+
{copied ? <Check className="text-success" weight="bold" /> : <Copy />}
|
|
56
|
+
</Button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/avatar.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
2
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "./utils";
|
|
5
|
+
|
|
6
|
+
export const Avatar = forwardRef<
|
|
7
|
+
ElementRef<typeof AvatarPrimitive.Root>,
|
|
8
|
+
ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
9
|
+
>(({ className, ...props }, ref) => (
|
|
10
|
+
<AvatarPrimitive.Root
|
|
11
|
+
ref={ref}
|
|
12
|
+
className={cn(
|
|
13
|
+
"relative flex size-8 shrink-0 overflow-hidden rounded-full border border-line",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
));
|
|
19
|
+
Avatar.displayName = "Avatar";
|
|
20
|
+
|
|
21
|
+
export const AvatarImage = forwardRef<
|
|
22
|
+
ElementRef<typeof AvatarPrimitive.Image>,
|
|
23
|
+
ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
24
|
+
>(({ className, ...props }, ref) => (
|
|
25
|
+
<AvatarPrimitive.Image
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={cn("aspect-square size-full object-cover", className)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
));
|
|
31
|
+
AvatarImage.displayName = "AvatarImage";
|
|
32
|
+
|
|
33
|
+
export const AvatarFallback = forwardRef<
|
|
34
|
+
ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
35
|
+
ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
36
|
+
>(({ className, ...props }, ref) => (
|
|
37
|
+
<AvatarPrimitive.Fallback
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={cn(
|
|
40
|
+
"flex size-full items-center justify-center bg-elevated text-xs font-medium text-muted",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
));
|
|
46
|
+
AvatarFallback.displayName = "AvatarFallback";
|
package/src/badge.tsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import type { HTMLAttributes } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "./utils";
|
|
5
|
+
|
|
6
|
+
const badgeVariants = cva(
|
|
7
|
+
"inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
neutral: "border-line bg-elevated text-muted",
|
|
12
|
+
outline: "border-line bg-transparent text-muted",
|
|
13
|
+
success: "border-success/30 bg-success/10 text-success",
|
|
14
|
+
warning: "border-warning/30 bg-warning/10 text-warning",
|
|
15
|
+
error: "border-error/30 bg-error/10 text-error",
|
|
16
|
+
info: "border-info/30 bg-info/10 text-info",
|
|
17
|
+
primary: "border-primary/30 bg-primary/10 text-primary",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: { variant: "neutral" },
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const dotVariants = cva("size-1.5 shrink-0 rounded-full", {
|
|
25
|
+
variants: {
|
|
26
|
+
variant: {
|
|
27
|
+
neutral: "bg-subtle",
|
|
28
|
+
outline: "bg-subtle",
|
|
29
|
+
success: "bg-success",
|
|
30
|
+
warning: "bg-warning",
|
|
31
|
+
error: "bg-error",
|
|
32
|
+
info: "bg-info",
|
|
33
|
+
primary: "bg-primary",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: { variant: "neutral" },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export interface BadgeProps
|
|
40
|
+
extends HTMLAttributes<HTMLSpanElement>,
|
|
41
|
+
VariantProps<typeof badgeVariants> {
|
|
42
|
+
dot?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function Badge({ className, variant, dot = false, children, ...props }: BadgeProps) {
|
|
46
|
+
return (
|
|
47
|
+
<span className={cn(badgeVariants({ variant }), className)} {...props}>
|
|
48
|
+
{dot ? <span className={cn(dotVariants({ variant }))} aria-hidden /> : null}
|
|
49
|
+
{children}
|
|
50
|
+
</span>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { badgeVariants };
|
package/src/button.tsx
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { Spinner } from "./spinner";
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
const buttonVariants = cva(
|
|
9
|
+
"relative inline-flex select-none items-center justify-center gap-[var(--control-gap)] whitespace-nowrap rounded-[var(--control-radius)] border border-transparent font-medium transition-[color,background-color,border-color,box-shadow,transform] duration-[120ms] active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus/40 focus-visible:ring-offset-1 focus-visible:ring-offset-canvas disabled:pointer-events-none disabled:opacity-50",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
primary:
|
|
14
|
+
"btn-fill btn-inner-border border-0 [--button-inner:var(--button-primary-inner)] bg-[var(--button-primary-bg)] text-[var(--button-primary-fg)] hover:bg-[var(--button-primary-hover)] active:bg-[var(--button-primary-active)]",
|
|
15
|
+
secondary:
|
|
16
|
+
"btn-fill btn-inner-border btn-interactive border-0 bg-[var(--button-secondary-bg)] text-fg hover:bg-[var(--button-surface-hover)] active:bg-[var(--button-surface-active)]",
|
|
17
|
+
outline:
|
|
18
|
+
"btn-interactive border-[var(--button-outline-border)] bg-transparent text-fg hover:border-fg/12 hover:bg-[var(--button-surface-hover)] active:border-fg/12 active:bg-[var(--button-surface-active)] dark:hover:border-[var(--button-outline-border)] dark:active:border-[var(--button-outline-border)]",
|
|
19
|
+
ghost:
|
|
20
|
+
"btn-inner-border btn-interactive border-0 text-muted hover:bg-[var(--button-surface-hover)] hover:text-fg active:bg-[var(--button-surface-active)]",
|
|
21
|
+
destructive:
|
|
22
|
+
"btn-fill btn-inner-border border-0 [--button-inner:var(--button-destructive-inner)] bg-[var(--button-destructive-bg)] text-[var(--button-destructive-fg)] hover:bg-[var(--button-destructive-hover)] active:bg-[var(--button-destructive-active)]",
|
|
23
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
24
|
+
},
|
|
25
|
+
size: {
|
|
26
|
+
xs: "h-[var(--control-h-xs)] [--control-gap:var(--control-gap-xs)] px-[var(--control-pad-x-xs)] text-xs [&_svg]:size-[var(--control-icon-xs)]",
|
|
27
|
+
sm: "h-[var(--control-h-sm)] px-[var(--control-pad-x-sm)] text-sm [&_svg]:size-[var(--control-icon-sm)]",
|
|
28
|
+
md: "h-[var(--control-h-md)] px-[var(--control-pad-x-md)] text-sm [&_svg]:size-[var(--control-icon-md)]",
|
|
29
|
+
lg: "h-[var(--control-h-lg)] px-[var(--control-pad-x-lg)] text-sm [&_svg]:size-[var(--control-icon-lg)]",
|
|
30
|
+
xl: "rounded-[var(--control-radius-xl)] h-[var(--control-h-xl)] px-[var(--control-pad-x-xl)] text-base [&_svg]:size-[var(--control-icon-xl)]",
|
|
31
|
+
icon: "h-[var(--control-h-md)] w-[var(--control-h-md)] p-0 [&_svg]:size-[var(--control-icon-md)]",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
variant: "primary",
|
|
36
|
+
size: "md",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/** Icon metrics per size, applied to icon wrappers so both stay aligned. */
|
|
42
|
+
const iconSize: Record<string, string> = {
|
|
43
|
+
xs: "[&_svg]:size-[var(--control-icon-xs)]",
|
|
44
|
+
sm: "[&_svg]:size-[var(--control-icon-sm)]",
|
|
45
|
+
md: "[&_svg]:size-[var(--control-icon-md)]",
|
|
46
|
+
lg: "[&_svg]:size-[var(--control-icon-lg)]",
|
|
47
|
+
xl: "[&_svg]:size-[var(--control-icon-xl)]",
|
|
48
|
+
icon: "[&_svg]:size-[var(--control-icon-md)]",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface ButtonProps
|
|
52
|
+
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
|
53
|
+
VariantProps<typeof buttonVariants> {
|
|
54
|
+
asChild?: boolean;
|
|
55
|
+
/** Optional icon rendered before the label. */
|
|
56
|
+
leadingIcon?: ReactNode;
|
|
57
|
+
/** Optional icon rendered after the label (e.g. a chevron). */
|
|
58
|
+
trailingIcon?: ReactNode;
|
|
59
|
+
/** Square, content-free button. Provide `aria-label`. */
|
|
60
|
+
iconOnly?: boolean;
|
|
61
|
+
/** Show a spinner and block interaction while an action runs. */
|
|
62
|
+
loading?: boolean;
|
|
63
|
+
/** Stretch to the container width. */
|
|
64
|
+
fullWidth?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
68
|
+
(
|
|
69
|
+
{
|
|
70
|
+
className,
|
|
71
|
+
variant,
|
|
72
|
+
size = "md",
|
|
73
|
+
asChild = false,
|
|
74
|
+
leadingIcon,
|
|
75
|
+
trailingIcon,
|
|
76
|
+
iconOnly = false,
|
|
77
|
+
loading = false,
|
|
78
|
+
fullWidth = false,
|
|
79
|
+
disabled,
|
|
80
|
+
children,
|
|
81
|
+
...props
|
|
82
|
+
},
|
|
83
|
+
ref,
|
|
84
|
+
) => {
|
|
85
|
+
const Comp = asChild ? Slot : "button";
|
|
86
|
+
const icon = iconSize[size ?? "md"];
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Comp
|
|
90
|
+
ref={ref}
|
|
91
|
+
aria-busy={loading || undefined}
|
|
92
|
+
disabled={asChild ? undefined : disabled || loading}
|
|
93
|
+
className={cn(
|
|
94
|
+
buttonVariants({ variant, size }),
|
|
95
|
+
iconOnly && "aspect-square px-0",
|
|
96
|
+
fullWidth && "w-full",
|
|
97
|
+
className,
|
|
98
|
+
)}
|
|
99
|
+
{...props}
|
|
100
|
+
>
|
|
101
|
+
{asChild ? (
|
|
102
|
+
children
|
|
103
|
+
) : (
|
|
104
|
+
<>
|
|
105
|
+
{loading ? (
|
|
106
|
+
<Spinner className={cn("text-current", icon)} aria-hidden />
|
|
107
|
+
) : leadingIcon ? (
|
|
108
|
+
<span className={cn("inline-flex shrink-0", icon)} aria-hidden>
|
|
109
|
+
{leadingIcon}
|
|
110
|
+
</span>
|
|
111
|
+
) : null}
|
|
112
|
+
{children}
|
|
113
|
+
{!loading && trailingIcon ? (
|
|
114
|
+
<span className={cn("inline-flex shrink-0", icon)} aria-hidden>
|
|
115
|
+
{trailingIcon}
|
|
116
|
+
</span>
|
|
117
|
+
) : null}
|
|
118
|
+
</>
|
|
119
|
+
)}
|
|
120
|
+
</Comp>
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
Button.displayName = "Button";
|
|
125
|
+
|
|
126
|
+
export { buttonVariants };
|
package/src/callout.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CheckCircle,
|
|
3
|
+
Info,
|
|
4
|
+
Warning,
|
|
5
|
+
XCircle,
|
|
6
|
+
} from "@phosphor-icons/react";
|
|
7
|
+
import type { Icon } from "@phosphor-icons/react";
|
|
8
|
+
import type { ReactNode } from "react";
|
|
9
|
+
|
|
10
|
+
import { cn } from "./utils";
|
|
11
|
+
|
|
12
|
+
type CalloutVariant = "info" | "success" | "warning" | "error";
|
|
13
|
+
|
|
14
|
+
const variants: Record<
|
|
15
|
+
CalloutVariant,
|
|
16
|
+
{ Icon: Icon; className: string; iconClassName: string }
|
|
17
|
+
> = {
|
|
18
|
+
info: {
|
|
19
|
+
Icon: Info,
|
|
20
|
+
className: "border-info/30 bg-info/10",
|
|
21
|
+
iconClassName: "text-info",
|
|
22
|
+
},
|
|
23
|
+
success: {
|
|
24
|
+
Icon: CheckCircle,
|
|
25
|
+
className: "border-success/30 bg-success/10",
|
|
26
|
+
iconClassName: "text-success",
|
|
27
|
+
},
|
|
28
|
+
warning: {
|
|
29
|
+
Icon: Warning,
|
|
30
|
+
className: "border-warning/30 bg-warning/10",
|
|
31
|
+
iconClassName: "text-warning",
|
|
32
|
+
},
|
|
33
|
+
error: {
|
|
34
|
+
Icon: XCircle,
|
|
35
|
+
className: "border-error/30 bg-error/10",
|
|
36
|
+
iconClassName: "text-error",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
interface CalloutProps {
|
|
41
|
+
variant?: CalloutVariant;
|
|
42
|
+
title?: ReactNode;
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
icon?: ReactNode;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function Callout({
|
|
49
|
+
variant = "info",
|
|
50
|
+
title,
|
|
51
|
+
children,
|
|
52
|
+
icon,
|
|
53
|
+
className,
|
|
54
|
+
}: CalloutProps) {
|
|
55
|
+
const { Icon: DefaultIcon, className: tone, iconClassName } = variants[variant];
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
role={variant === "error" ? "alert" : undefined}
|
|
59
|
+
className={cn(
|
|
60
|
+
"flex items-start gap-2.5 rounded-lg border px-3.5 py-3 text-sm",
|
|
61
|
+
tone,
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<span className={cn("mt-0.5 shrink-0 [&_svg]:size-4", iconClassName)}>
|
|
66
|
+
{icon ?? <DefaultIcon weight="fill" />}
|
|
67
|
+
</span>
|
|
68
|
+
<div className="min-w-0 space-y-0.5">
|
|
69
|
+
{title ? <p className="font-medium text-fg">{title}</p> : null}
|
|
70
|
+
{children ? <div className="text-muted">{children}</div> : null}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
package/src/card.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
6
|
+
({ className, ...props }, ref) => (
|
|
7
|
+
<div
|
|
8
|
+
ref={ref}
|
|
9
|
+
className={cn(
|
|
10
|
+
"rounded-card border border-line bg-surface shadow-xs",
|
|
11
|
+
className,
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
Card.displayName = "Card";
|
|
18
|
+
|
|
19
|
+
export const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
20
|
+
({ className, ...props }, ref) => (
|
|
21
|
+
<div
|
|
22
|
+
ref={ref}
|
|
23
|
+
className={cn("flex flex-col gap-1 border-b border-line px-5 py-4", className)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
CardHeader.displayName = "CardHeader";
|
|
29
|
+
|
|
30
|
+
export const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
|
|
31
|
+
({ className, ...props }, ref) => (
|
|
32
|
+
<h2 ref={ref} className={cn("text-sm font-semibold text-fg", className)} {...props} />
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
CardTitle.displayName = "CardTitle";
|
|
36
|
+
|
|
37
|
+
export const CardDescription = forwardRef<
|
|
38
|
+
HTMLParagraphElement,
|
|
39
|
+
HTMLAttributes<HTMLParagraphElement>
|
|
40
|
+
>(({ className, ...props }, ref) => (
|
|
41
|
+
<p ref={ref} className={cn("text-sm text-muted", className)} {...props} />
|
|
42
|
+
));
|
|
43
|
+
CardDescription.displayName = "CardDescription";
|
|
44
|
+
|
|
45
|
+
export const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
46
|
+
({ className, ...props }, ref) => (
|
|
47
|
+
<div ref={ref} className={cn("px-5 py-4", className)} {...props} />
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
CardContent.displayName = "CardContent";
|
|
51
|
+
|
|
52
|
+
export const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
53
|
+
({ className, ...props }, ref) => (
|
|
54
|
+
<div
|
|
55
|
+
ref={ref}
|
|
56
|
+
className={cn("flex items-center gap-3 border-t border-line px-5 py-3", className)}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
CardFooter.displayName = "CardFooter";
|
package/src/checkbox.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
2
|
+
import { Check, Minus } from "@phosphor-icons/react";
|
|
3
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
export const Checkbox = forwardRef<
|
|
8
|
+
ElementRef<typeof CheckboxPrimitive.Root>,
|
|
9
|
+
ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
|
10
|
+
>(({ className, ...props }, ref) => (
|
|
11
|
+
<CheckboxPrimitive.Root
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn(
|
|
14
|
+
"peer size-4 shrink-0 rounded border border-line-strong bg-surface transition-colors",
|
|
15
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus/40",
|
|
16
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
17
|
+
"data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-fg",
|
|
18
|
+
"data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-fg",
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
|
|
24
|
+
{props.checked === "indeterminate" ? (
|
|
25
|
+
<Minus className="size-3" weight="bold" />
|
|
26
|
+
) : (
|
|
27
|
+
<Check className="size-3" weight="bold" />
|
|
28
|
+
)}
|
|
29
|
+
</CheckboxPrimitive.Indicator>
|
|
30
|
+
</CheckboxPrimitive.Root>
|
|
31
|
+
));
|
|
32
|
+
Checkbox.displayName = "Checkbox";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogBody,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogPanel,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
} from "./dialog";
|
|
14
|
+
import { RadioCard, RadioGroup } from "./radio-group";
|
|
15
|
+
|
|
16
|
+
export interface ChoiceOption {
|
|
17
|
+
value: string;
|
|
18
|
+
title: ReactNode;
|
|
19
|
+
description?: ReactNode;
|
|
20
|
+
badge?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ChoiceDialogProps {
|
|
24
|
+
open?: boolean;
|
|
25
|
+
onOpenChange?: (open: boolean) => void;
|
|
26
|
+
/** Optional element that opens the dialog. */
|
|
27
|
+
trigger?: ReactNode;
|
|
28
|
+
/** Dialog heading. */
|
|
29
|
+
title: string;
|
|
30
|
+
/** Selectable options, rendered as radio cards. */
|
|
31
|
+
options: ChoiceOption[];
|
|
32
|
+
/** Initially selected value (uncontrolled). */
|
|
33
|
+
defaultValue?: string;
|
|
34
|
+
/** Selected value (controlled). */
|
|
35
|
+
value?: string;
|
|
36
|
+
onValueChange?: (value: string) => void;
|
|
37
|
+
/** Footer labels. */
|
|
38
|
+
cancelLabel?: string;
|
|
39
|
+
confirmLabel?: string;
|
|
40
|
+
/** Called with the selected value when the confirm button is pressed. */
|
|
41
|
+
onConfirm?: (value: string) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Dialog for picking one option from a small set, styled as the
|
|
46
|
+
* "Create production instance" reference: divided header, radio cards, and a
|
|
47
|
+
* footer with an equal-width secondary Cancel plus primary Continue.
|
|
48
|
+
*/
|
|
49
|
+
export function ChoiceDialog({
|
|
50
|
+
open,
|
|
51
|
+
onOpenChange,
|
|
52
|
+
trigger,
|
|
53
|
+
title,
|
|
54
|
+
options,
|
|
55
|
+
defaultValue,
|
|
56
|
+
value,
|
|
57
|
+
onValueChange,
|
|
58
|
+
cancelLabel = "Cancel",
|
|
59
|
+
confirmLabel = "Continue",
|
|
60
|
+
onConfirm,
|
|
61
|
+
}: ChoiceDialogProps) {
|
|
62
|
+
const [internal, setInternal] = useState(defaultValue ?? options[0]?.value ?? "");
|
|
63
|
+
const selected = value ?? internal;
|
|
64
|
+
|
|
65
|
+
const select = (next: string) => {
|
|
66
|
+
if (value === undefined) setInternal(next);
|
|
67
|
+
onValueChange?.(next);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
72
|
+
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
|
|
73
|
+
<DialogContent
|
|
74
|
+
aria-describedby={undefined}
|
|
75
|
+
className="max-w-[29.4rem] [&>button]:hidden"
|
|
76
|
+
>
|
|
77
|
+
<DialogPanel>
|
|
78
|
+
<DialogHeader>
|
|
79
|
+
<DialogTitle className="text-xl">{title}</DialogTitle>
|
|
80
|
+
</DialogHeader>
|
|
81
|
+
<DialogBody>
|
|
82
|
+
<RadioGroup label={title} value={selected} onChange={select}>
|
|
83
|
+
{options.map((option) => (
|
|
84
|
+
<RadioCard
|
|
85
|
+
key={option.value}
|
|
86
|
+
value={option.value}
|
|
87
|
+
title={option.title}
|
|
88
|
+
description={option.description}
|
|
89
|
+
badge={option.badge}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
</RadioGroup>
|
|
93
|
+
</DialogBody>
|
|
94
|
+
</DialogPanel>
|
|
95
|
+
<DialogFooter className="gap-3 pt-2.5">
|
|
96
|
+
<Button
|
|
97
|
+
variant="secondary"
|
|
98
|
+
className="flex-1"
|
|
99
|
+
onClick={() => onOpenChange?.(false)}
|
|
100
|
+
>
|
|
101
|
+
{cancelLabel}
|
|
102
|
+
</Button>
|
|
103
|
+
<Button className="flex-1" onClick={() => onConfirm?.(selected)}>
|
|
104
|
+
{confirmLabel}
|
|
105
|
+
</Button>
|
|
106
|
+
</DialogFooter>
|
|
107
|
+
</DialogContent>
|
|
108
|
+
</Dialog>
|
|
109
|
+
);
|
|
110
|
+
}
|