From 8774bb171dfb621d1d5db273a1dad93530822f70 Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Tue, 17 Jun 2025 12:05:48 -0400 Subject: [PATCH 1/5] moved with v1 of docs --- .../design/components/code.example.tsx | 33 ++++++++ .../content/design/components/code.mdx | 82 +++++++++++++++++++ internal/ui/src/components/code.tsx | 29 +++++++ internal/ui/src/index.ts | 1 + 4 files changed, 145 insertions(+) create mode 100644 apps/engineering/content/design/components/code.example.tsx create mode 100644 apps/engineering/content/design/components/code.mdx create mode 100644 internal/ui/src/components/code.tsx diff --git a/apps/engineering/content/design/components/code.example.tsx b/apps/engineering/content/design/components/code.example.tsx new file mode 100644 index 0000000000..280c88f373 --- /dev/null +++ b/apps/engineering/content/design/components/code.example.tsx @@ -0,0 +1,33 @@ +import { Code } from "@unkey/ui"; +import { RenderComponentWithSnippet } from "@/app/components/render"; + +export function CodeExample() { + return ( + +
+
+
+ const greeting = "Hello, World!"; + function add(a, b) {'{'} return a + b; {'}'} + npm install @unkey/ui +
+
+
+
+ ); +} + +export function CodeVariants() { + return ( + +
+
+
+ Default Variant + Outline Variant +
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/engineering/content/design/components/code.mdx b/apps/engineering/content/design/components/code.mdx new file mode 100644 index 0000000000..003ef8ec5e --- /dev/null +++ b/apps/engineering/content/design/components/code.mdx @@ -0,0 +1,82 @@ +--- +title: Code +description: A versatile code component for displaying inline and block code snippets with customizable styling. +--- + +import { CodeExample, CodeVariants } from "./code.example"; + +# Code + +The Code component provides a consistent way to display code snippets within your application. It supports both inline and block code display with customizable styling options. + +## Features + +- Multiple variants (default, outline) +- Consistent monospace font +- Customizable styling +- Accessible by default +- Responsive design +- Focus states for better interaction + +## Usage + +```tsx +import { Code } from "@unkey/ui"; + +function MyComponent() { + return const hello = "world";; +} +``` + +## Examples + +### Default Variant + +The default variant provides a subtle background with a border. + + + +### Variant Styles + +The Code component supports different visual styles. + + + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `variant` | `"default" \| "outline"` | `"default"` | The visual style of the code component | +| `className` | `string` | `undefined` | Additional CSS classes to customize the component | + +The component also accepts all standard HTML pre element props. + +## Styling + +The Code component includes: + +- Monospace font for code readability +- Consistent padding and border radius +- Focus states for keyboard navigation +- Hover effects for better interaction +- Dark mode support +- Responsive design + +## Best Practices + +- Use the default variant for inline code snippets +- Use the outline variant when you need more visual separation +- Add appropriate padding around the code component +- Consider using syntax highlighting for complex code blocks +- Ensure sufficient contrast between code and background +- Use semantic HTML structure for better accessibility + +## Accessibility + +The Code component is built with accessibility in mind: + +- Proper ARIA attributes +- Keyboard focus management +- High contrast text +- Semantic HTML structure +- Screen reader support \ No newline at end of file diff --git a/internal/ui/src/components/code.tsx b/internal/ui/src/components/code.tsx new file mode 100644 index 0000000000..9fdc14697e --- /dev/null +++ b/internal/ui/src/components/code.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { type VariantProps, cva } from "class-variance-authority"; +import { cn } from "../lib/utils"; + +const codeVariants = cva( + "inline-flex font-mono items-center rounded-md border border-border bg-transparent px-2.5 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: " text-primary bg-background-subtle hover:border-primary", + + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface CodeProps + extends React.HTMLAttributes, + VariantProps {} + +function Code({ className, variant, ...props }: CodeProps) { + return
;
+}
+
+export { Code, codeVariants };
diff --git a/internal/ui/src/index.ts b/internal/ui/src/index.ts
index c7083321b0..e4c12c5b18 100644
--- a/internal/ui/src/index.ts
+++ b/internal/ui/src/index.ts
@@ -1,5 +1,6 @@
 export * from "./components/button";
 export * from "./components/card";
+export * from "./components/code";
 export * from "./components/copy-button";
 export * from "./components/date-time/date-time";
 export * from "./components/dialog/dialog-container";

From cc8566dc02b8dd951628d5f8f4fd5b6a0462a0bc Mon Sep 17 00:00:00 2001
From: Michael Silva 
Date: Wed, 18 Jun 2025 10:04:47 -0400
Subject: [PATCH 2/5] small changes

---
 .../content/design/components/code.example.tsx            | 8 +++++---
 internal/ui/src/components/code.tsx                       | 2 ++
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/apps/engineering/content/design/components/code.example.tsx b/apps/engineering/content/design/components/code.example.tsx
index 280c88f373..d1be27b26d 100644
--- a/apps/engineering/content/design/components/code.example.tsx
+++ b/apps/engineering/content/design/components/code.example.tsx
@@ -1,5 +1,5 @@
-import { Code } from "@unkey/ui";
 import { RenderComponentWithSnippet } from "@/app/components/render";
+import { Code } from "@unkey/ui";
 
 export function CodeExample() {
   return (
@@ -8,7 +8,9 @@ export function CodeExample() {
         
const greeting = "Hello, World!"; - function add(a, b) {'{'} return a + b; {'}'} + + function add(a, b) {"{"} return a + b; {"}"} + npm install @unkey/ui
@@ -30,4 +32,4 @@ export function CodeVariants() { ); -} \ No newline at end of file +} diff --git a/internal/ui/src/components/code.tsx b/internal/ui/src/components/code.tsx index 9fdc14697e..854c148007 100644 --- a/internal/ui/src/components/code.tsx +++ b/internal/ui/src/components/code.tsx @@ -26,4 +26,6 @@ function Code({ className, variant, ...props }: CodeProps) { return
;
 }
 
+Code.displayName = "Code";
+
 export { Code, codeVariants };

From ada90e2e255681f0427ad1c7d9c5733f4100f3ff Mon Sep 17 00:00:00 2001
From: Michael Silva 
Date: Fri, 20 Jun 2025 07:59:28 -0400
Subject: [PATCH 3/5] eng docs change

---
 .../design/components/code.example.tsx        | 37 +++++++++++++++----
 internal/ui/src/components/code.tsx           |  3 +-
 2 files changed, 32 insertions(+), 8 deletions(-)

diff --git a/apps/engineering/content/design/components/code.example.tsx b/apps/engineering/content/design/components/code.example.tsx
index d1be27b26d..dd71a982bc 100644
--- a/apps/engineering/content/design/components/code.example.tsx
+++ b/apps/engineering/content/design/components/code.example.tsx
@@ -1,17 +1,40 @@
+"use client";
 import { RenderComponentWithSnippet } from "@/app/components/render";
-import { Code } from "@unkey/ui";
-
+import { Code, CopyButton, VisibleButton } from "@unkey/ui";
+import { useState } from "react";
+const EXAMPLE_SNIPPET = `curl -XPOST 'https://api.unkey.dev/v1/ratelimits.limit' \\
+  -H 'Content-Type: application/json' \\
+  -H 'Authorization: Bearer ' \\
+    -d '{
+      "namespace": "demo_namespace",
+    "identifier": "user_123",
+    "limit": 10,
+    "duration": 10000
+}'`;
 export function CodeExample() {
+  const [showKeyInSnippet, setShowKeyInSnippet] = useState(false);
   return (
     
       
-
- const greeting = "Hello, World!"; - - function add(a, b) {"{"} return a + b; {"}"} +
+ + {showKeyInSnippet + ? EXAMPLE_SNIPPET + : EXAMPLE_SNIPPET.replace("", "********")} +
+ setShowKeyInSnippet(visible)} + title="Key Snippet" + /> + + +
- npm install @unkey/ui
diff --git a/internal/ui/src/components/code.tsx b/internal/ui/src/components/code.tsx index 854c148007..45ebc84f38 100644 --- a/internal/ui/src/components/code.tsx +++ b/internal/ui/src/components/code.tsx @@ -1,5 +1,6 @@ -import * as React from "react"; +"use client"; import { type VariantProps, cva } from "class-variance-authority"; +import type * as React from "react"; import { cn } from "../lib/utils"; const codeVariants = cva( From 5aab8ffec340a15a354a123b829818732ed82814 Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Fri, 20 Jun 2025 08:50:53 -0400 Subject: [PATCH 4/5] remove old code and replace imports --- apps/dashboard/app/(app)/identities/[identityId]/page.tsx | 3 +-- .../app/(app)/settings/billing/stripe/checkout/page.tsx | 2 +- .../app/(app)/settings/billing/stripe/portal/page.tsx | 3 +-- apps/dashboard/app/(app)/settings/root-keys/new/client.tsx | 2 +- apps/dashboard/app/(app)/settings/vercel/page.tsx | 3 +-- apps/dashboard/app/integrations/vercel/callback/page.tsx | 3 +-- apps/dashboard/app/new/create-ratelimit.tsx | 3 +-- apps/dashboard/app/new/keys.tsx | 2 +- 8 files changed, 8 insertions(+), 13 deletions(-) diff --git a/apps/dashboard/app/(app)/identities/[identityId]/page.tsx b/apps/dashboard/app/(app)/identities/[identityId]/page.tsx index 6dfebedc4f..04fa52faf0 100644 --- a/apps/dashboard/app/(app)/identities/[identityId]/page.tsx +++ b/apps/dashboard/app/(app)/identities/[identityId]/page.tsx @@ -3,7 +3,6 @@ import { notFound } from "next/navigation"; import { PageContent } from "@/components/page-content"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; -import { Code } from "@/components/ui/code"; import { Table, TableBody, @@ -16,7 +15,7 @@ import { getAuth } from "@/lib/auth"; import { clickhouse } from "@/lib/clickhouse"; import { db } from "@/lib/db"; import { formatNumber } from "@/lib/fmt"; -import { Button, CopyButton } from "@unkey/ui"; +import { Button, Code, CopyButton } from "@unkey/ui"; import { ChevronRight, Minus } from "lucide-react"; import ms from "ms"; import Link from "next/link"; diff --git a/apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx b/apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx index 1479075312..a962d10d25 100644 --- a/apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx +++ b/apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx @@ -1,7 +1,7 @@ -import { Code } from "@/components/ui/code"; import { getAuth } from "@/lib/auth"; import { db } from "@/lib/db"; import { stripeEnv } from "@/lib/env"; +import { Code } from "@unkey/ui"; import { Empty } from "@unkey/ui"; import { redirect } from "next/navigation"; import Stripe from "stripe"; diff --git a/apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx b/apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx index 6ed1514060..a1546711af 100644 --- a/apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx +++ b/apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx @@ -1,8 +1,7 @@ -import { Code } from "@/components/ui/code"; import { getAuth } from "@/lib/auth"; import { db } from "@/lib/db"; import { stripeEnv } from "@/lib/env"; -import { Empty } from "@unkey/ui"; +import { Code, Empty } from "@unkey/ui"; import { redirect } from "next/navigation"; import Stripe from "stripe"; diff --git a/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx b/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx index 6012d25926..a9fe65c8b1 100644 --- a/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx +++ b/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx @@ -1,6 +1,5 @@ "use client"; -import { Code } from "@/components/ui/code"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Dialog, @@ -22,6 +21,7 @@ import { CardHeader, CardTitle, Checkbox, + Code, CopyButton, Input, VisibleButton, diff --git a/apps/dashboard/app/(app)/settings/vercel/page.tsx b/apps/dashboard/app/(app)/settings/vercel/page.tsx index 7a1c3cf096..62399cdc00 100644 --- a/apps/dashboard/app/(app)/settings/vercel/page.tsx +++ b/apps/dashboard/app/(app)/settings/vercel/page.tsx @@ -6,12 +6,11 @@ import { Navbar as SubMenu } from "@/components/dashboard/navbar"; import { Navigation } from "@/components/navigation/navigation"; import { PageContent } from "@/components/page-content"; -import { Code } from "@/components/ui/code"; import { getAuth } from "@/lib/auth"; import { auth } from "@/lib/auth/server"; import { type Api, type Key, type VercelBinding, db, eq, schema } from "@/lib/db"; import { Gear } from "@unkey/icons"; -import { Button, Empty } from "@unkey/ui"; +import { Button, Code, Empty } from "@unkey/ui"; import { Vercel } from "@unkey/vercel"; import Link from "next/link"; import { notFound } from "next/navigation"; diff --git a/apps/dashboard/app/integrations/vercel/callback/page.tsx b/apps/dashboard/app/integrations/vercel/callback/page.tsx index ac6d366f36..a8aea9f74d 100644 --- a/apps/dashboard/app/integrations/vercel/callback/page.tsx +++ b/apps/dashboard/app/integrations/vercel/callback/page.tsx @@ -3,11 +3,10 @@ * Hiding for now until we decide if we want to fix it up or toss it */ -import { Code } from "@/components/ui/code"; import { getAuth } from "@/lib/auth"; import { db, eq, schema } from "@/lib/db"; import { vercelIntegrationEnv } from "@/lib/env"; -import { Empty } from "@unkey/ui"; +import { Code, Empty } from "@unkey/ui"; import { Vercel } from "@unkey/vercel"; import { Client } from "./client"; import { exchangeCode } from "./exchange-code"; diff --git a/apps/dashboard/app/new/create-ratelimit.tsx b/apps/dashboard/app/new/create-ratelimit.tsx index 3b07b642db..049f1bab6e 100644 --- a/apps/dashboard/app/new/create-ratelimit.tsx +++ b/apps/dashboard/app/new/create-ratelimit.tsx @@ -1,9 +1,8 @@ -import { Code } from "@/components/ui/code"; import { getCurrentUser } from "@/lib/auth"; import { router } from "@/lib/trpc/routers"; import { createCallerFactory } from "@trpc/server"; import type { Workspace } from "@unkey/db"; -import { Button, CopyButton } from "@unkey/ui"; +import { Button, Code, CopyButton } from "@unkey/ui"; import { GlobeLock } from "lucide-react"; import Link from "next/link"; diff --git a/apps/dashboard/app/new/keys.tsx b/apps/dashboard/app/new/keys.tsx index f15cebccd3..4556196bcf 100644 --- a/apps/dashboard/app/new/keys.tsx +++ b/apps/dashboard/app/new/keys.tsx @@ -1,7 +1,6 @@ "use client"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Code } from "@/components/ui/code"; import { trpc } from "@/lib/trpc/client"; import { Button, @@ -11,6 +10,7 @@ import { CardFooter, CardHeader, CardTitle, + Code, CopyButton, Empty, Separator, From b5c7d16511dc5beb02fe9f270711bf4fc10aadf8 Mon Sep 17 00:00:00 2001 From: MichaelUnkey Date: Tue, 24 Jun 2025 16:30:47 -0400 Subject: [PATCH 5/5] copy button and style changes --- .../components/key-created-success-dialog.tsx | 32 +- .../create-key/components/secret-key.tsx | 15 +- .../[keyId]/components/table/logs-table.tsx | 8 +- .../(app)/identities/[identityId]/page.tsx | 2 +- .../settings/billing/stripe/checkout/page.tsx | 3 +- .../(app)/settings/root-keys/new/client.tsx | 348 ++++++++++++------ apps/dashboard/app/new/create-ratelimit.tsx | 6 +- .../navigation/copyable-id-button.tsx | 1 + .../buttons/copy-button.examples.tsx | 35 +- .../design/components/buttons/copy-button.mdx | 63 +++- .../design/components/code.example.tsx | 50 +-- .../content/design/components/code.mdx | 58 ++- internal/ui/src/components/code.tsx | 50 ++- internal/ui/src/components/copy-button.tsx | 39 +- 14 files changed, 482 insertions(+), 228 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx b/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx index 7db8649d06..73e06b4ee7 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx @@ -4,7 +4,7 @@ import { ConfirmPopover } from "@/components/confirmation-popover"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { toast } from "@/components/ui/toaster"; import { ArrowRight, Check, CircleInfo, Key2, Plus } from "@unkey/icons"; -import { Button, CopyButton, InfoTooltip, VisibleButton } from "@unkey/ui"; +import { Button, Code, CopyButton, InfoTooltip, VisibleButton } from "@unkey/ui"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { UNNAMED_KEY } from "../create-key.constants"; @@ -220,27 +220,15 @@ export const KeyCreatedSuccessDialog = ({
Try It Out
-
-
-
-
-                      {showKeyInSnippet ? snippet : snippet.replace(keyData.key, maskedKey)}
-                    
-
-
- setShowKeyInSnippet(visible)} - title="Key Snippet" - /> - -
-
-
+ + + } + copyButton={} + > + {showKeyInSnippet ? snippet : snippet.replace(keyData.key, maskedKey)} +
diff --git a/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/secret-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/secret-key.tsx index 06c74d5302..2f4d86aab6 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/secret-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/secret-key.tsx @@ -2,7 +2,7 @@ import { cn } from "@/lib/utils"; import { CircleLock } from "@unkey/icons"; -import { Button, CopyButton, VisibleButton } from "@unkey/ui"; +import { CopyButton, VisibleButton } from "@unkey/ui"; import { useState } from "react"; const maskKey = (key: string): string => { @@ -45,17 +45,8 @@ export const SecretKey = ({ setIsVisible={(visible) => setIsVisible(visible)} title={title} /> - + +
diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx index 26706fae17..9d9f2f072b 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx @@ -324,7 +324,7 @@ export const KeyDetailsLogsTable = ({ keyspaceId, keyId, selectedLog, onLogSelec className="pointer-events-auto" onClick={(e) => e.stopPropagation()} > - + @@ -336,7 +336,7 @@ export const KeyDetailsLogsTable = ({ keyspaceId, keyId, selectedLog, onLogSelec className="pointer-events-auto flex-shrink-0" onClick={(e) => e.stopPropagation()} > - + )} @@ -380,7 +380,7 @@ export const KeyDetailsLogsTable = ({ keyspaceId, keyId, selectedLog, onLogSelec className="pointer-events-auto" onClick={(e) => e.stopPropagation()} > - + @@ -392,7 +392,7 @@ export const KeyDetailsLogsTable = ({ keyspaceId, keyId, selectedLog, onLogSelec className="pointer-events-auto flex-shrink-0" onClick={(e) => e.stopPropagation()} > - + )} diff --git a/apps/dashboard/app/(app)/identities/[identityId]/page.tsx b/apps/dashboard/app/(app)/identities/[identityId]/page.tsx index f318f2f1d4..9baca8075d 100644 --- a/apps/dashboard/app/(app)/identities/[identityId]/page.tsx +++ b/apps/dashboard/app/(app)/identities/[identityId]/page.tsx @@ -83,7 +83,7 @@ export default async function Page(props: Props) {

Meta

{identity.meta ? ( - {JSON.stringify(identity.meta, null, 2)} + {JSON.stringify(identity.meta, null, 2)} ) : ( No metadata diff --git a/apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx b/apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx index a962d10d25..7986bd4a69 100644 --- a/apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx +++ b/apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx @@ -1,8 +1,7 @@ import { getAuth } from "@/lib/auth"; import { db } from "@/lib/db"; import { stripeEnv } from "@/lib/env"; -import { Code } from "@unkey/ui"; -import { Empty } from "@unkey/ui"; +import { Code, Empty } from "@unkey/ui"; import { redirect } from "next/navigation"; import Stripe from "stripe"; diff --git a/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx b/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx index a9fe65c8b1..cbef5af73b 100644 --- a/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx +++ b/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx @@ -1,17 +1,12 @@ "use client"; +import { ConfirmPopover } from "@/components/confirmation-popover"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { toast } from "@/components/ui/toaster"; import { trpc } from "@/lib/trpc/client"; +import { Check, CircleInfo, Key2 } from "@unkey/icons"; import { type UnkeyPermission, unkeyPermissionValidation } from "@unkey/rbac"; import { Button, @@ -23,13 +18,15 @@ import { Checkbox, Code, CopyButton, + InfoTooltip, Input, VisibleButton, } from "@unkey/ui"; -import { ChevronRight } from "lucide-react"; +import { ArrowRight, ChevronRight } from "lucide-react"; import { useRouter } from "next/navigation"; import { createParser, parseAsArrayOf, useQueryState } from "nuqs"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; +import { SecretKey } from "../../../apis/[apiId]/_components/create-key/components/secret-key"; import { apiPermissions, workspacePermissions } from "../[keyId]/permissions/permissions"; type Props = { @@ -39,6 +36,36 @@ type Props = { }[]; }; +type PermissionToggleProps = { + checked: boolean; + setChecked: (checked: boolean) => void; + label: string | React.ReactNode; + description: string; +}; + +const PermissionToggle: React.FC = ({ + checked, + setChecked, + label, + description, +}) => { + return ( +
+
+ { + setChecked(!checked); + }} + /> + +
+ +

{description}

+
+ ); +}; + const parseAsUnkeyPermission = createParser({ parse(queryValue) { const { success, data } = unkeyPermissionValidation.safeParse(queryValue); @@ -47,9 +74,17 @@ const parseAsUnkeyPermission = createParser({ serialize: String, }); +const UNNAMED_KEY = "Unnamed Key"; + export const Client: React.FC = ({ apis }) => { const router = useRouter(); const [name, setName] = useState(undefined); + const [showKeyInSnippet, setShowKeyInSnippet] = useState(false); + const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const [pendingAction, setPendingAction] = useState< + "close" | "create-another" | "go-to-details" | null + >(null); + const dividerRef = useRef(null); const [selectedPermissions, setSelectedPermissions] = useQueryState( "permissions", @@ -68,16 +103,18 @@ export const Client: React.FC = ({ apis }) => { }); const snippet = `curl -XPOST '${process.env.NEXT_PUBLIC_UNKEY_API_URL ?? "https://api.unkey.dev"}/v1/keys.createKey' \\ - -H 'Authorization: Bearer ${key.data?.key}' \\ - -H 'Content-Type: application/json' \\ - -d '{ - "prefix": "hello", - "apiId": "" - }'`; - - const maskedKey = `unkey_${"*".repeat(key.data?.key.split("_").at(1)?.length ?? 0)}`; - const [showKey, setShowKey] = useState(false); - const [showKeyInSnippet, setShowKeyInSnippet] = useState(false); + -H 'Authorization: Bearer ${key.data?.key}' \\ + -H 'Content-Type: application/json' \\ + -d '{ + "prefix": "hello", + "apiId": "" + }'`; + + const split = key.data?.key?.split("_") ?? []; + const maskedKey = + split.length >= 2 + ? `${split.at(0)}_${"*".repeat(split.at(1)?.length ?? 0)}` + : "*".repeat(split.at(0)?.length ?? 0); const handleSetChecked = (permission: UnkeyPermission, checked: boolean) => { setSelectedPermissions((prevPermissions) => { @@ -120,6 +157,56 @@ export const Client: React.FC = ({ apis }) => { setCardStatesMap(initialCardStates); }, []); // Execute ones on the first load + const handleCloseAttempt = (action: "close" | "create-another" | "go-to-details" = "close") => { + setPendingAction(action); + setIsConfirmOpen(true); + }; + + const handleConfirmClose = () => { + if (!pendingAction) { + console.error("No pending action when confirming close"); + return; + } + + setIsConfirmOpen(false); + + try { + // Always close the dialog first + key.reset(); + setSelectedPermissions([]); + setName(""); + + // Then execute the specific action + switch (pendingAction) { + case "create-another": + // Reset form for creating another key + break; + + case "go-to-details": + router.push("/settings/root-keys"); + break; + + default: + // Dialog already closed, nothing more to do + router.push("/settings/root-keys"); + break; + } + } catch (error) { + console.error("Error executing pending action:", error); + toast.error("Action Failed", { + description: "An unexpected error occurred. Please try again.", + }); + } finally { + setPendingAction(null); + } + }; + + const handleDialogOpenChange = (open: boolean) => { + if (!open) { + handleCloseAttempt("close"); + } + }; + return (
@@ -159,7 +246,7 @@ export const Client: React.FC = ({ apis }) => { label={{category}} description={`Select all permissions for ${category} in this workspace`} checked={isAllSelected} - setChecked={(isChecked) => { + setChecked={(isChecked: boolean) => { allPermissionNames.forEach((permission) => { handleSetChecked(permission, isChecked); }); @@ -174,7 +261,7 @@ export const Client: React.FC = ({ apis }) => { label={action} description={description} checked={selectedPermissions.includes(permission)} - setChecked={(isChecked) => handleSetChecked(permission, isChecked)} + setChecked={(isChecked: boolean) => handleSetChecked(permission, isChecked)} /> ))}
@@ -232,7 +319,7 @@ export const Client: React.FC = ({ apis }) => { } description={`Select all ${category} permissions for this API`} checked={isAllSelected} - setChecked={(isChecked) => { + setChecked={(isChecked: boolean) => { allPermissionNames.forEach((permission) => { handleSetChecked(permission, isChecked); }); @@ -247,7 +334,9 @@ export const Client: React.FC = ({ apis }) => { label={action} description={description} checked={selectedPermissions.includes(permission)} - setChecked={(isChecked) => handleSetChecked(permission, isChecked)} + setChecked={(isChecked: boolean) => + handleSetChecked(permission, isChecked) + } /> ))} @@ -271,93 +360,140 @@ export const Client: React.FC = ({ apis }) => { Create New Key - { - if (!v) { - // Remove the key from memory when closing the modal - key.reset(); - setSelectedPermissions([]); - setName(""); - router.push("/settings/root-keys"); - } - }} - > - - - Your API Key - - This key is only shown once and can not be recovered. Please store it somewhere safe. - - - - {showKey ? key.data?.key : maskedKey} -
- + handleCloseAttempt("close")} + > + <> +
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+ Root Key Created +
+
+ You've successfully generated a new Root key. +
+
+
+
+
+
+
Key Details
+
+
+
+ +
+
+
{key.data?.keyId}
+ +
+ {name ?? UNNAMED_KEY} +
+
+
+ +
+
+
+
+
Key Secret
+ - +
+ + + Copy and save this key secret as it won't be shown again.{" "} + + Learn more + + +
+
+
+
Try It Out
+ + } + copyButton={} + > + {showKeyInSnippet ? snippet : snippet.replace(key.data?.key ?? "", maskedKey)} + +
+
+
+ All set! You can now create another key or explore the docs to learn more +
- - - -

- Try creating a new api key for your users: -

- -
- - -
-
- {showKeyInSnippet ? snippet : snippet.replace(key.data?.key ?? "", maskedKey)}
-
- - - + e.preventDefault(), + }} + /> +
); }; - -type PermissionToggleProps = { - checked: boolean; - setChecked: (checked: boolean) => void; - label: string | React.ReactNode; - description: string; -}; - -const PermissionToggle: React.FC = ({ - checked, - setChecked, - label, - description, -}) => { - return ( -
-
- { - setChecked(!checked); - }} - /> - -
- -

{description}

-
- ); -}; diff --git a/apps/dashboard/app/new/create-ratelimit.tsx b/apps/dashboard/app/new/create-ratelimit.tsx index 049f1bab6e..cfdb818527 100644 --- a/apps/dashboard/app/new/create-ratelimit.tsx +++ b/apps/dashboard/app/new/create-ratelimit.tsx @@ -83,9 +83,11 @@ export const CreateRatelimit: React.FC = async (props) => { The following request will limit the user to 10 requests per 10 seconds.

- + } + > {snippet} - diff --git a/apps/dashboard/components/navigation/copyable-id-button.tsx b/apps/dashboard/components/navigation/copyable-id-button.tsx index 2ba96e6f96..caff482e4a 100644 --- a/apps/dashboard/components/navigation/copyable-id-button.tsx +++ b/apps/dashboard/components/navigation/copyable-id-button.tsx @@ -69,6 +69,7 @@ export const CopyableIDButton = ({ value, className = "" }: CopyableIDButtonProp {value} { const apiKey = "uk_1234567890abcdef"; + return ( -
-
- Basic usage: - +
+
+
+ Basic usage: + +
+
+ {apiKey} + +
-
- {apiKey} - + +
+
+ Different variants: + + + +
+ +
+ With custom styling: + +
diff --git a/apps/engineering/content/design/components/buttons/copy-button.mdx b/apps/engineering/content/design/components/buttons/copy-button.mdx index e710e1e13e..9ce262c55d 100644 --- a/apps/engineering/content/design/components/buttons/copy-button.mdx +++ b/apps/engineering/content/design/components/buttons/copy-button.mdx @@ -1,38 +1,67 @@ --- title: CopyButton -summary: A button component that copies text to clipboard with visual feedback +summary: A button component that copies text to clipboard with visual feedback and accessibility features --- import { Default } from "./copy-button.examples" -The `CopyButton` component provides a simple way to copy text to the clipboard with visual feedback. When clicked, it copies the provided text and shows a checkmark icon to indicate success. +The `CopyButton` component provides a simple way to copy text to the clipboard with visual feedback. It extends the base Button component and includes built-in copy functionality with proper accessibility support. ## Features -- Visual feedback with icon change on copy -- Accessible with screen reader support +- Visual feedback with icon change on copy (TaskUnchecked → TaskChecked) +- Accessible with screen reader support and ARIA labels - Automatic reset after 2 seconds -- Customizable through className prop -- TypeScript support +- Extends ButtonProps for full button customization +- Analytics support with optional src prop +- Prevents event propagation to parent elements +- TypeScript support with proper typing ## Props -| Prop | Type | Description | -|------|------|-------------| -| `value` | `string` | The text content to be copied to clipboard | -| `src` | `string?` | Optional source identifier for analytics | -| `className` | `string?` | Additional CSS classes to apply to the button | +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `value` | `string` | **Required** | The text content to be copied to clipboard | +| `src` | `string?` | `undefined` | Optional source identifier for analytics tracking | +| `variant` | `ButtonVariant` | `"outline"` | Button variant (outline, ghost, primary, etc.) | +| `className` | `string?` | `undefined` | Additional CSS classes to apply to the button | + +The component also accepts all standard Button props. ## Usage +## Behavior + +1. **Click Handling**: On click, the button copies the provided text to the clipboard +2. **Visual Feedback**: The icon changes from TaskUnchecked to TaskChecked state +3. **Auto Reset**: After 2 seconds, the icon reverts to its original state +4. **Event Prevention**: Parent click events are prevented from triggering (`e.stopPropagation`) +5. **Analytics**: If provided, the `src` prop is passed to analytics for tracking + ## Accessibility -The component includes screen reader support with an appropriate "Copy" label. The visual state change (unchecked to checked icon) provides clear feedback for all users. +The component includes comprehensive accessibility features: -## Behavior +- **ARIA Label**: `aria-label="Copy to clipboard"` for screen readers +- **Screen Reader Text**: Hidden "Copy" text for additional context +- **Title Attribute**: `title="Copy to clipboard"` for tooltip on hover +- **Focus Management**: Proper focus states with `focus:ring-0 focus:border-grayA-6` +- **Keyboard Support**: Full keyboard navigation support inherited from Button component + +## Styling + +The component includes default styling: + +- Fixed dimensions: `w-6 h-6` (24x24px) +- Focus states: `focus:ring-0 focus:border-grayA-6` +- Icons: Full-size icons with `w-full h-full` +- Default variant: `outline` for subtle appearance + +## Best Practices -1. On click, the button copies the provided text to the clipboard -2. The icon changes from an unchecked to checked state -3. After 2 seconds, the icon reverts to its original state -4. Parent click events are prevented from triggering (e.stopPropagation) +- Use the `src` prop for analytics tracking when copying sensitive data +- Provide meaningful `value` content for better user experience +- Consider the button's context when choosing variants +- Ensure sufficient contrast for the button in your design +- Test with screen readers to verify accessibility diff --git a/apps/engineering/content/design/components/code.example.tsx b/apps/engineering/content/design/components/code.example.tsx index dd71a982bc..848d40d98f 100644 --- a/apps/engineering/content/design/components/code.example.tsx +++ b/apps/engineering/content/design/components/code.example.tsx @@ -2,40 +2,43 @@ import { RenderComponentWithSnippet } from "@/app/components/render"; import { Code, CopyButton, VisibleButton } from "@unkey/ui"; import { useState } from "react"; + const EXAMPLE_SNIPPET = `curl -XPOST 'https://api.unkey.dev/v1/ratelimits.limit' \\ -H 'Content-Type: application/json' \\ -H 'Authorization: Bearer ' \\ - -d '{ - "namespace": "demo_namespace", + -d '{ + "namespace": "demo_namespace", "identifier": "user_123", "limit": 10, "duration": 10000 -}'`; + }'`; + export function CodeExample() { const [showKeyInSnippet, setShowKeyInSnippet] = useState(false); + return (
-
- - {showKeyInSnippet - ? EXAMPLE_SNIPPET - : EXAMPLE_SNIPPET.replace("", "********")} -
- setShowKeyInSnippet(visible)} - title="Key Snippet" - /> - - -
-
-
+ + } + visibleButton={ + setShowKeyInSnippet(visible)} + title="Key Snippet" + /> + } + > + {showKeyInSnippet + ? EXAMPLE_SNIPPET + : EXAMPLE_SNIPPET.replace("", "********")} +
@@ -49,7 +52,8 @@ export function CodeVariants() {
Default Variant - Outline Variant + Ghost Variant + Legacy Variant
diff --git a/apps/engineering/content/design/components/code.mdx b/apps/engineering/content/design/components/code.mdx index 003ef8ec5e..82eeb26688 100644 --- a/apps/engineering/content/design/components/code.mdx +++ b/apps/engineering/content/design/components/code.mdx @@ -1,17 +1,18 @@ --- title: Code -description: A versatile code component for displaying inline and block code snippets with customizable styling. +description: A versatile code component for displaying inline and block code snippets with customizable styling and integrated buttons. --- import { CodeExample, CodeVariants } from "./code.example"; # Code -The Code component provides a consistent way to display code snippets within your application. It supports both inline and block code display with customizable styling options. +The Code component provides a consistent way to display code snippets within your application. It supports both inline and block code display with customizable styling options and integrated button functionality. ## Features -- Multiple variants (default, outline) +- Multiple variants (default, ghost, legacy) +- Integrated copy and visibility buttons - Consistent monospace font - Customizable styling - Accessible by default @@ -21,18 +22,25 @@ The Code component provides a consistent way to display code snippets within you ## Usage ```tsx -import { Code } from "@unkey/ui"; +import { Code, CopyButton, VisibleButton } from "@unkey/ui"; function MyComponent() { - return const hello = "world";; + return ( + } + visibleButton={ {}} />} + > + const hello = "world"; + + ); } ``` ## Examples -### Default Variant +### Default Variant with Buttons -The default variant provides a subtle background with a border. +The default variant provides a subtle background with a border and integrated buttons. @@ -46,11 +54,33 @@ The Code component supports different visual styles. | Prop | Type | Default | Description | |------|------|---------|-------------| -| `variant` | `"default" \| "outline"` | `"default"` | The visual style of the code component | -| `className` | `string` | `undefined` | Additional CSS classes to customize the component | +| `variant` | `"default" \| "ghost" \| "legacy"` | `"default"` | The visual style of the code component | +| `className` | `string` | `undefined` | Additional CSS classes for the wrapper div | +| `buttonsClassName` | `string` | `undefined` | Additional CSS classes for the buttons container | +| `preClassName` | `string` | `undefined` | Additional CSS classes for the pre element | +| `copyButton` | `React.ReactNode` | `undefined` | Copy button component to display | +| `visibleButton` | `React.ReactNode` | `undefined` | Visibility toggle button component | The component also accepts all standard HTML pre element props. +## Variants + +### Default +- Subtle background with border +- White background in light mode, black in dark mode +- Gray border for visual separation + +### Ghost +- Transparent background +- No border +- Minimal visual impact + +### Legacy +- Primary text color +- Subtle background +- Hover effects with primary border +- Rounded corners + ## Styling The Code component includes: @@ -61,15 +91,18 @@ The Code component includes: - Hover effects for better interaction - Dark mode support - Responsive design +- Flexible button positioning ## Best Practices -- Use the default variant for inline code snippets -- Use the outline variant when you need more visual separation +- Use the default variant for code snippets with buttons +- Use the ghost variant when you need minimal visual impact +- Use the legacy variant for backward compatibility - Add appropriate padding around the code component - Consider using syntax highlighting for complex code blocks - Ensure sufficient contrast between code and background - Use semantic HTML structure for better accessibility +- Position buttons appropriately using the buttonsClassName prop ## Accessibility @@ -79,4 +112,5 @@ The Code component is built with accessibility in mind: - Keyboard focus management - High contrast text - Semantic HTML structure -- Screen reader support \ No newline at end of file +- Screen reader support +- Focus states for interactive elements \ No newline at end of file diff --git a/internal/ui/src/components/code.tsx b/internal/ui/src/components/code.tsx index 45ebc84f38..332eec6e63 100644 --- a/internal/ui/src/components/code.tsx +++ b/internal/ui/src/components/code.tsx @@ -4,13 +4,15 @@ import type * as React from "react"; import { cn } from "../lib/utils"; const codeVariants = cva( - "inline-flex font-mono items-center rounded-md border border-border bg-transparent px-2.5 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + "flex w-full px-4 border rounded-xl ring-0 flex items-start justify-between ph-no-capture whitespace-pre-wrap break-all ", { variants: { variant: { - default: " text-primary bg-background-subtle hover:border-primary", - - outline: "text-foreground", + default: + "border-grayA-5 focus:outline-none focus:ring-0 bg-white dark:bg-black text-[11px] py-2", + ghost: "border-none bg-transparent text-[11px] py-2", + legacy: + "text-primary bg-background-subtle rounded-md hover:border-primary focus:outline-none focus:ring-0 border-grayA-4", }, }, defaultVariants: { @@ -21,10 +23,44 @@ const codeVariants = cva( export interface CodeProps extends React.HTMLAttributes, - VariantProps {} + VariantProps { + className?: string; + buttonsClassName?: string; + preClassName?: string; + variant?: "default" | "ghost" | "legacy"; + copyButton?: React.ReactNode; + visibleButton?: React.ReactNode; +} -function Code({ className, variant, ...props }: CodeProps) { - return
;
+function Code({
+  className,
+  variant,
+  copyButton,
+  visibleButton,
+  buttonsClassName,
+  preClassName,
+  ...props
+}: CodeProps) {
+  return (
+    
+
+      
+ {visibleButton} + {copyButton} +
+
+ ); } Code.displayName = "Code"; diff --git a/internal/ui/src/components/copy-button.tsx b/internal/ui/src/components/copy-button.tsx index 4f0a456a72..381ad7487a 100644 --- a/internal/ui/src/components/copy-button.tsx +++ b/internal/ui/src/components/copy-button.tsx @@ -2,18 +2,25 @@ import { TaskChecked, TaskUnchecked } from "@unkey/icons"; import * as React from "react"; import { cn } from "../lib/utils"; +import { Button, type ButtonProps } from "./button"; -interface CopyButtonProps extends React.HTMLAttributes { +type CopyButtonProps = ButtonProps & { + /** + * The value to copy to clipboard + */ value: string; + /** + * Source component for analytics + */ src?: string; -} +}; async function copyToClipboardWithMeta(value: string, _meta?: Record) { navigator.clipboard.writeText(value); } export const CopyButton = React.forwardRef( - ({ value, className, src, ...props }, ref) => { + ({ value, src, variant = "outline", className, onClick, ...props }, ref) => { const [copied, setCopied] = React.useState(false); React.useEffect(() => { @@ -27,18 +34,24 @@ export const CopyButton = React.forwardRef( }, [copied]); return ( - + ); }, );