@jinaan/uikit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +63 -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 +84 -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 +1522 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/input.d.ts +19 -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 +589 -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 +171 -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/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@jinaan/uikit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
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 };
@@ -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";
@@ -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
+ }