Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions app/showcase/components/copyable-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
Copyable,
CopyableButton,
CopyableContent,
} from "@registry/bases/base/ui/copyable";

import { Example } from "./example";

// The hero — the canonical Copyable use case: a CLI install command sitting in a
// card surface with a copy button that confirms with a check.
export function CopyablePreview() {
return (
<Copyable className="w-full max-w-md" value="npx shadcn@latest add @crescent-ui/copyable">
<CopyableContent>npx shadcn@latest add @crescent-ui/copyable</CopyableContent>
<CopyableButton />
</Copyable>
);
}

// The additional usages, each its own flat example.
export function CopyableExamples() {
return (
<div className="space-y-8">
<Example
name="Secret value"
description="Any string — an API key, a token, a URL — copies on press; the value shown can differ from what's copied."
>
<Copyable className="w-full max-w-md" value="crsnt_demo_3f8a1c9b2e7d406192a5">
<CopyableContent>crsnt_demo_••••••••••••••••••••</CopyableContent>
<CopyableButton />
</Copyable>
</Example>
<Example
name="Custom content"
description="CopyableContent is just a slot — drop in a label, badge, or any markup beside the value."
>
<Copyable className="w-full max-w-md" value="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB">
<CopyableContent className="flex flex-col gap-0.5">
<span className="text-xs font-medium text-muted-foreground">
Public key
</span>
<span className="truncate">ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB</span>
</CopyableContent>
<CopyableButton />
</Copyable>
</Example>
</div>
);
}
9 changes: 9 additions & 0 deletions app/showcase/components/demos.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChoiceboxExamples, ChoiceboxPreview } from "./choicebox-demo";
import { CopyableExamples, CopyablePreview } from "./copyable-demo";

// The gallery's source of truth: one entry per published crescent-ui component.
// `preview` is the hero shown in the preview frame; `examples` are the extra
Expand All @@ -22,6 +23,14 @@ export const demos: Record<
preview: <ChoiceboxPreview />,
examples: <ChoiceboxExamples />,
},
copyable: {
title: "Copyable",
description:
"A card-styled surface that displays a value alongside a button that copies it to the clipboard and confirms with a check.",
sourceFile: "ui/copyable.tsx",
preview: <CopyablePreview />,
examples: <CopyableExamples />,
},
};

export const demoOrder = Object.keys(demos);
16 changes: 16 additions & 0 deletions registry-dist/base-luma/copyable.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "copyable",
"type": "registry:ui",
"title": "Copyable",
"description": "A card-styled surface that displays a value alongside a button that copies it to the clipboard.",
"dependencies": [],
"files": [
{
"path": "registry/base-luma/ui/copyable.tsx",
"type": "registry:ui",
"target": "components/ui/copyable.tsx",
"content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-<name>.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-<name>` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// <Copyable value=\"npm i crescent-ui\">\n// <CopyableContent>npm i crescent-ui</CopyableContent>\n// <CopyableButton />\n// </Copyable>\n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in <CopyableContent> (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext<CopyableContextValue | null>(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within <Copyable>.\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n <CopyableContext.Provider value={context}>\n <div\n data-slot=\"copyable\"\n data-copied={copied ? \"\" : undefined}\n className={cn(\"gap-2 rounded-3xl border border-transparent bg-muted/50 px-4 py-2.5 text-foreground shadow-sm flex items-center\", className)}\n {...props}\n />\n </CopyableContext.Provider>\n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"copyable-content\"\n className={cn(\"truncate font-mono text-sm text-foreground min-w-0 flex-1\", className)}\n {...props}\n />\n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n <button\n type=\"button\"\n data-slot=\"copyable-button\"\n data-copied={copied ? \"\" : undefined}\n onClick={copy}\n aria-label={copied ? \"Copied\" : \"Copy to clipboard\"}\n className={cn(\n \"size-9 rounded-full bg-transparent text-muted-foreground hover:bg-background hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/30 data-copied:text-primary group/copyable-button inline-flex shrink-0 items-center justify-center outline-none transition-[color,box-shadow,background-color] disabled:pointer-events-none disabled:opacity-50 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className,\n )}\n {...props}\n >\n {children ??\n (copied ? (\n // The confirmation tick is drawn from the active icon library. As a\n // registry component this is an IconPlaceholder: the showcase resolves\n // it live against the selected library, and a consumer's `shadcn add`\n // rewrites it to their own iconLibrary's check.\n <IconPlaceholder\n lucide=\"Check\"\n tabler=\"IconCheck\"\n phosphor=\"Check\"\n hugeicons=\"Tick02Icon\"\n remixicon=\"RiCheckLine\"\n aria-hidden\n />\n ) : (\n <IconPlaceholder\n lucide=\"Copy\"\n tabler=\"IconCopy\"\n phosphor=\"Copy\"\n hugeicons=\"Copy01Icon\"\n remixicon=\"RiFileCopyLine\"\n aria-hidden\n />\n ))}\n </button>\n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
}
]
}
16 changes: 16 additions & 0 deletions registry-dist/base-lyra/copyable.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "copyable",
"type": "registry:ui",
"title": "Copyable",
"description": "A card-styled surface that displays a value alongside a button that copies it to the clipboard.",
"dependencies": [],
"files": [
{
"path": "registry/base-lyra/ui/copyable.tsx",
"type": "registry:ui",
"target": "components/ui/copyable.tsx",
"content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-<name>.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-<name>` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// <Copyable value=\"npm i crescent-ui\">\n// <CopyableContent>npm i crescent-ui</CopyableContent>\n// <CopyableButton />\n// </Copyable>\n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in <CopyableContent> (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext<CopyableContextValue | null>(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within <Copyable>.\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n <CopyableContext.Provider value={context}>\n <div\n data-slot=\"copyable\"\n data-copied={copied ? \"\" : undefined}\n className={cn(\"gap-1.5 rounded-none border border-input bg-card px-2.5 py-1.5 text-card-foreground flex items-center\", className)}\n {...props}\n />\n </CopyableContext.Provider>\n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"copyable-content\"\n className={cn(\"truncate font-mono text-xs text-foreground min-w-0 flex-1\", className)}\n {...props}\n />\n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n <button\n type=\"button\"\n data-slot=\"copyable-button\"\n data-copied={copied ? \"\" : undefined}\n onClick={copy}\n aria-label={copied ? \"Copied\" : \"Copy to clipboard\"}\n className={cn(\n \"size-7 rounded-none bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring/50 data-copied:text-primary group/copyable-button inline-flex shrink-0 items-center justify-center outline-none transition-[color,box-shadow,background-color] disabled:pointer-events-none disabled:opacity-50 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className,\n )}\n {...props}\n >\n {children ??\n (copied ? (\n // The confirmation tick is drawn from the active icon library. As a\n // registry component this is an IconPlaceholder: the showcase resolves\n // it live against the selected library, and a consumer's `shadcn add`\n // rewrites it to their own iconLibrary's check.\n <IconPlaceholder\n lucide=\"Check\"\n tabler=\"IconCheck\"\n phosphor=\"Check\"\n hugeicons=\"Tick02Icon\"\n remixicon=\"RiCheckLine\"\n aria-hidden\n />\n ) : (\n <IconPlaceholder\n lucide=\"Copy\"\n tabler=\"IconCopy\"\n phosphor=\"Copy\"\n hugeicons=\"Copy01Icon\"\n remixicon=\"RiFileCopyLine\"\n aria-hidden\n />\n ))}\n </button>\n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
}
]
}
Loading