diff --git a/app/showcase/components/copyable-demo.tsx b/app/showcase/components/copyable-demo.tsx new file mode 100644 index 0000000..28370df --- /dev/null +++ b/app/showcase/components/copyable-demo.tsx @@ -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 ( + + npx shadcn@latest add @crescent-ui/copyable + + + ); +} + +// The additional usages, each its own flat example. +export function CopyableExamples() { + return ( +
+ + + crsnt_demo_•••••••••••••••••••• + + + + + + + + Public key + + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB + + + + +
+ ); +} diff --git a/app/showcase/components/demos.tsx b/app/showcase/components/demos.tsx index a567204..4fedd26 100644 --- a/app/showcase/components/demos.tsx +++ b/app/showcase/components/demos.tsx @@ -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 @@ -22,6 +23,14 @@ export const demos: Record< preview: , examples: , }, + 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: , + examples: , + }, }; export const demoOrder = Object.keys(demos); diff --git a/registry-dist/base-luma/copyable.json b/registry-dist/base-luma/copyable.json new file mode 100644 index 0000000..aee1da4 --- /dev/null +++ b/registry-dist/base-luma/copyable.json @@ -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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/base-lyra/copyable.json b/registry-dist/base-lyra/copyable.json new file mode 100644 index 0000000..90d1aa6 --- /dev/null +++ b/registry-dist/base-lyra/copyable.json @@ -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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/base-maia/copyable.json b/registry-dist/base-maia/copyable.json new file mode 100644 index 0000000..fbb52ba --- /dev/null +++ b/registry-dist/base-maia/copyable.json @@ -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-maia/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/base-mira/copyable.json b/registry-dist/base-mira/copyable.json new file mode 100644 index 0000000..2c9ce1f --- /dev/null +++ b/registry-dist/base-mira/copyable.json @@ -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-mira/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/base-nova/copyable.json b/registry-dist/base-nova/copyable.json new file mode 100644 index 0000000..b0d7013 --- /dev/null +++ b/registry-dist/base-nova/copyable.json @@ -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-nova/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/base-rhea/copyable.json b/registry-dist/base-rhea/copyable.json new file mode 100644 index 0000000..853a4ff --- /dev/null +++ b/registry-dist/base-rhea/copyable.json @@ -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-rhea/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/base-sera/copyable.json b/registry-dist/base-sera/copyable.json new file mode 100644 index 0000000..27f888e --- /dev/null +++ b/registry-dist/base-sera/copyable.json @@ -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-sera/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/base-vega/copyable.json b/registry-dist/base-vega/copyable.json new file mode 100644 index 0000000..6bcc090 --- /dev/null +++ b/registry-dist/base-vega/copyable.json @@ -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-vega/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/radix-luma/copyable.json b/registry-dist/radix-luma/copyable.json new file mode 100644 index 0000000..5dd5be3 --- /dev/null +++ b/registry-dist/radix-luma/copyable.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "copyable", + "type": "registry:ui", + "title": "Copyable (Radix)", + "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).", + "dependencies": [], + "files": [ + { + "path": "registry/radix-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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/radix-lyra/copyable.json b/registry-dist/radix-lyra/copyable.json new file mode 100644 index 0000000..fc91438 --- /dev/null +++ b/registry-dist/radix-lyra/copyable.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "copyable", + "type": "registry:ui", + "title": "Copyable (Radix)", + "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).", + "dependencies": [], + "files": [ + { + "path": "registry/radix-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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/radix-maia/copyable.json b/registry-dist/radix-maia/copyable.json new file mode 100644 index 0000000..e37b248 --- /dev/null +++ b/registry-dist/radix-maia/copyable.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "copyable", + "type": "registry:ui", + "title": "Copyable (Radix)", + "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).", + "dependencies": [], + "files": [ + { + "path": "registry/radix-maia/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/radix-mira/copyable.json b/registry-dist/radix-mira/copyable.json new file mode 100644 index 0000000..e21280c --- /dev/null +++ b/registry-dist/radix-mira/copyable.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "copyable", + "type": "registry:ui", + "title": "Copyable (Radix)", + "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).", + "dependencies": [], + "files": [ + { + "path": "registry/radix-mira/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/radix-nova/copyable.json b/registry-dist/radix-nova/copyable.json new file mode 100644 index 0000000..8875ee8 --- /dev/null +++ b/registry-dist/radix-nova/copyable.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "copyable", + "type": "registry:ui", + "title": "Copyable (Radix)", + "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).", + "dependencies": [], + "files": [ + { + "path": "registry/radix-nova/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/radix-rhea/copyable.json b/registry-dist/radix-rhea/copyable.json new file mode 100644 index 0000000..f5daed7 --- /dev/null +++ b/registry-dist/radix-rhea/copyable.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "copyable", + "type": "registry:ui", + "title": "Copyable (Radix)", + "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).", + "dependencies": [], + "files": [ + { + "path": "registry/radix-rhea/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/radix-sera/copyable.json b/registry-dist/radix-sera/copyable.json new file mode 100644 index 0000000..4081a36 --- /dev/null +++ b/registry-dist/radix-sera/copyable.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "copyable", + "type": "registry:ui", + "title": "Copyable (Radix)", + "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).", + "dependencies": [], + "files": [ + { + "path": "registry/radix-sera/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry-dist/radix-vega/copyable.json b/registry-dist/radix-vega/copyable.json new file mode 100644 index 0000000..7239004 --- /dev/null +++ b/registry-dist/radix-vega/copyable.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "copyable", + "type": "registry:ui", + "title": "Copyable (Radix)", + "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).", + "dependencies": [], + "files": [ + { + "path": "registry/radix-vega/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-.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-` 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// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (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(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\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 \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\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 \n ) : (\n \n ))}\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n" + } + ] +} diff --git a/registry.json b/registry.json index fa227f3..9996b6b 100644 --- a/registry.json +++ b/registry.json @@ -30,6 +30,34 @@ "target": "components/ui/choicebox.tsx" } ] + }, + { + "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 and confirms with a check. Fully theme-token driven and icon-library agnostic.", + "dependencies": [], + "files": [ + { + "path": "registry/bases/base/ui/copyable.tsx", + "type": "registry:ui", + "target": "components/ui/copyable.tsx" + } + ] + }, + { + "name": "copyable-radix", + "type": "registry:ui", + "title": "Copyable (Radix)", + "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency). Install this when your project's base is `radix`.", + "dependencies": [], + "files": [ + { + "path": "registry/bases/radix/ui/copyable.tsx", + "type": "registry:ui", + "target": "components/ui/copyable.tsx" + } + ] } ] } diff --git a/registry/bases/base/ui/copyable.tsx b/registry/bases/base/ui/copyable.tsx new file mode 100644 index 0000000..b909f1f --- /dev/null +++ b/registry/bases/base/ui/copyable.tsx @@ -0,0 +1,157 @@ +"use client"; + +import * as React from "react"; + +import { cn } from "@/lib/utils"; +import { IconPlaceholder } from "@/ui/icon-placeholder"; + +// Copyable — a card-styled "value + copy button" surface: a visualization of +// some text (an API key, an install command, a token) paired with a button that +// copies it to the clipboard and confirms with a check. +// +// Copyable has no Base UI / Radix primitive dependency — it's structural markup +// plus the Clipboard API — so the `base` and `radix` variants are byte-identical +// and kept in sync only so each project base can install it from its namespace. +// +// STYLING MODEL (shadcn cn-* system): +// The visual treatment of each part lives in `cn-copyable-*` semantic classes, +// defined per style under registry/styles/style-.css via `@apply`. Only +// structural layout (flex/positioning/behavior) is inline here. The registry +// build resolves each `cn-*` token into that style's utilities (see +// scripts/build-registry.ts); the showcase resolves them live via a +// `.style-` wrapper class. Keep ALL color/border/radius/size/typography +// in the style CSS — inline classes win twMerge conflicts and would override it. +// +// Anatomy (compound): +// +// npm i crescent-ui +// +// +// +// `value` is the string written to the clipboard; the displayed content is +// whatever you put in (usually the same text, but it can be a +// masked or prettier representation of the value). + +type CopyableContextValue = { + copied: boolean; + copy: () => void; +}; + +const CopyableContext = React.createContext(null); + +function useCopyable() { + const context = React.useContext(CopyableContext); + if (!context) { + throw new Error("Copyable parts must be used within ."); + } + return context; +} + +function Copyable({ + value, + timeout = 2000, + onCopy, + className, + ...props +}: React.ComponentProps<"div"> & { + /** The string written to the clipboard when the button is pressed. */ + value: string; + /** How long (ms) the "copied" confirmation stays before resetting. */ + timeout?: number; + /** Called after a successful copy. */ + onCopy?: (value: string) => void; +}) { + const [copied, setCopied] = React.useState(false); + + const copy = React.useCallback(() => { + // Guard for non-secure contexts / older browsers where the async Clipboard + // API is unavailable — fail silently rather than throwing in the handler. + if (!navigator.clipboard?.writeText) return; + void navigator.clipboard + .writeText(value) + .then(() => { + setCopied(true); + onCopy?.(value); + }) + .catch(() => {}); + }, [value, onCopy]); + + // Reset the confirmation after `timeout`; re-copying restarts the timer. + React.useEffect(() => { + if (!copied) return; + const id = window.setTimeout(() => setCopied(false), timeout); + return () => window.clearTimeout(id); + }, [copied, timeout]); + + const context = React.useMemo(() => ({ copied, copy }), [copied, copy]); + + return ( + +
+ + ); +} + +function CopyableContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CopyableButton({ + className, + children, + ...props +}: React.ComponentProps<"button">) { + const { copied, copy } = useCopyable(); + return ( + + ); +} + +export { Copyable, CopyableContent, CopyableButton, useCopyable }; diff --git a/registry/bases/radix/ui/copyable.tsx b/registry/bases/radix/ui/copyable.tsx new file mode 100644 index 0000000..b909f1f --- /dev/null +++ b/registry/bases/radix/ui/copyable.tsx @@ -0,0 +1,157 @@ +"use client"; + +import * as React from "react"; + +import { cn } from "@/lib/utils"; +import { IconPlaceholder } from "@/ui/icon-placeholder"; + +// Copyable — a card-styled "value + copy button" surface: a visualization of +// some text (an API key, an install command, a token) paired with a button that +// copies it to the clipboard and confirms with a check. +// +// Copyable has no Base UI / Radix primitive dependency — it's structural markup +// plus the Clipboard API — so the `base` and `radix` variants are byte-identical +// and kept in sync only so each project base can install it from its namespace. +// +// STYLING MODEL (shadcn cn-* system): +// The visual treatment of each part lives in `cn-copyable-*` semantic classes, +// defined per style under registry/styles/style-.css via `@apply`. Only +// structural layout (flex/positioning/behavior) is inline here. The registry +// build resolves each `cn-*` token into that style's utilities (see +// scripts/build-registry.ts); the showcase resolves them live via a +// `.style-` wrapper class. Keep ALL color/border/radius/size/typography +// in the style CSS — inline classes win twMerge conflicts and would override it. +// +// Anatomy (compound): +// +// npm i crescent-ui +// +// +// +// `value` is the string written to the clipboard; the displayed content is +// whatever you put in (usually the same text, but it can be a +// masked or prettier representation of the value). + +type CopyableContextValue = { + copied: boolean; + copy: () => void; +}; + +const CopyableContext = React.createContext(null); + +function useCopyable() { + const context = React.useContext(CopyableContext); + if (!context) { + throw new Error("Copyable parts must be used within ."); + } + return context; +} + +function Copyable({ + value, + timeout = 2000, + onCopy, + className, + ...props +}: React.ComponentProps<"div"> & { + /** The string written to the clipboard when the button is pressed. */ + value: string; + /** How long (ms) the "copied" confirmation stays before resetting. */ + timeout?: number; + /** Called after a successful copy. */ + onCopy?: (value: string) => void; +}) { + const [copied, setCopied] = React.useState(false); + + const copy = React.useCallback(() => { + // Guard for non-secure contexts / older browsers where the async Clipboard + // API is unavailable — fail silently rather than throwing in the handler. + if (!navigator.clipboard?.writeText) return; + void navigator.clipboard + .writeText(value) + .then(() => { + setCopied(true); + onCopy?.(value); + }) + .catch(() => {}); + }, [value, onCopy]); + + // Reset the confirmation after `timeout`; re-copying restarts the timer. + React.useEffect(() => { + if (!copied) return; + const id = window.setTimeout(() => setCopied(false), timeout); + return () => window.clearTimeout(id); + }, [copied, timeout]); + + const context = React.useMemo(() => ({ copied, copy }), [copied, copy]); + + return ( + +
+ + ); +} + +function CopyableContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CopyableButton({ + className, + children, + ...props +}: React.ComponentProps<"button">) { + const { copied, copy } = useCopyable(); + return ( + + ); +} + +export { Copyable, CopyableContent, CopyableButton, useCopyable }; diff --git a/registry/styles/style-luma.css b/registry/styles/style-luma.css index 3f93fad..c6d3b49 100644 --- a/registry/styles/style-luma.css +++ b/registry/styles/style-luma.css @@ -34,6 +34,19 @@ @apply text-xs/relaxed text-muted-foreground; } + /* Copyable — a value surface + copy button. Pillowy: very round, soft shadow. */ + .cn-copyable { + @apply gap-2 rounded-3xl border border-transparent bg-muted/50 px-4 py-2.5 text-foreground shadow-sm; + } + + .cn-copyable-content { + @apply truncate font-mono text-sm text-foreground; + } + + .cn-copyable-button { + @apply 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; + } + .cn-choicebox-item-indicator { @apply size-5 rounded-full data-[multiple]:rounded-lg border border-input bg-background text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground; } diff --git a/registry/styles/style-lyra.css b/registry/styles/style-lyra.css index 05ddbe7..e5a9997 100644 --- a/registry/styles/style-lyra.css +++ b/registry/styles/style-lyra.css @@ -33,6 +33,19 @@ @apply text-xs/relaxed text-muted-foreground; } + /* Copyable — a value surface + copy button. Compact, square, hairline focus. */ + .cn-copyable { + @apply gap-1.5 rounded-none border border-input bg-card px-2.5 py-1.5 text-card-foreground; + } + + .cn-copyable-content { + @apply truncate font-mono text-xs text-foreground; + } + + .cn-copyable-button { + @apply 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; + } + .cn-choicebox-item-indicator { @apply size-4 rounded-full data-[multiple]:rounded-none border border-input text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground; } diff --git a/registry/styles/style-maia.css b/registry/styles/style-maia.css index c1d8683..a6a17c3 100644 --- a/registry/styles/style-maia.css +++ b/registry/styles/style-maia.css @@ -33,6 +33,19 @@ @apply text-xs/relaxed text-muted-foreground; } + /* Copyable — a value surface + copy button. Very round, pill copy button. */ + .cn-copyable { + @apply gap-2 rounded-2xl border border-input bg-card px-4 py-2.5 text-card-foreground shadow-xs; + } + + .cn-copyable-content { + @apply truncate font-mono text-sm text-foreground; + } + + .cn-copyable-button { + @apply size-9 rounded-full bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/30 data-copied:text-primary; + } + .cn-choicebox-item-indicator { @apply size-5 rounded-full data-[multiple]:rounded-lg border border-input text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground; } diff --git a/registry/styles/style-mira.css b/registry/styles/style-mira.css index e16d88f..1964606 100644 --- a/registry/styles/style-mira.css +++ b/registry/styles/style-mira.css @@ -44,6 +44,19 @@ @apply text-xs/relaxed text-muted-foreground; } + /* Copyable — a value surface + copy button. Same soft, rounded temperament. */ + .cn-copyable { + @apply gap-2 rounded-lg border border-input bg-card px-3 py-2 text-card-foreground shadow-xs; + } + + .cn-copyable-content { + @apply truncate font-mono text-sm text-foreground; + } + + .cn-copyable-button { + @apply size-8 rounded-md bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 data-copied:text-primary; + } + /* Radio (single) stays an intrinsic circle; checkbox (multiple) tracks the soft radius. data-multiple is set by the component. */ .cn-choicebox-item-indicator { diff --git a/registry/styles/style-nova.css b/registry/styles/style-nova.css index f1f30f4..aa9ea70 100644 --- a/registry/styles/style-nova.css +++ b/registry/styles/style-nova.css @@ -32,6 +32,19 @@ @apply text-xs/relaxed text-muted-foreground; } + /* Copyable — a value surface + copy button. Flat tonal fill, no border. */ + .cn-copyable { + @apply gap-2 rounded-lg border border-transparent bg-muted/40 px-3 py-2 text-foreground; + } + + .cn-copyable-content { + @apply truncate font-mono text-sm text-foreground; + } + + .cn-copyable-button { + @apply size-8 rounded-lg bg-transparent text-muted-foreground hover:bg-background hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/30 data-copied:text-primary; + } + .cn-choicebox-item-indicator { @apply size-5 rounded-full data-[multiple]:rounded-sm border border-input text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground; } diff --git a/registry/styles/style-rhea.css b/registry/styles/style-rhea.css index cbff236..56a3ac8 100644 --- a/registry/styles/style-rhea.css +++ b/registry/styles/style-rhea.css @@ -33,6 +33,19 @@ @apply text-xs/relaxed text-muted-foreground; } + /* Copyable — a value surface + copy button. Generously round, squircle button. */ + .cn-copyable { + @apply gap-2 rounded-2xl border border-transparent bg-muted/40 px-4 py-2.5 text-foreground shadow-sm; + } + + .cn-copyable-content { + @apply truncate font-mono text-sm text-foreground; + } + + .cn-copyable-button { + @apply size-9 rounded-xl bg-transparent text-muted-foreground hover:bg-background hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/30 data-copied:text-primary; + } + .cn-choicebox-item-indicator { @apply size-5 rounded-full data-[multiple]:rounded-lg border border-input text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground; } diff --git a/registry/styles/style-sera.css b/registry/styles/style-sera.css index 80fac8c..0ae5aee 100644 --- a/registry/styles/style-sera.css +++ b/registry/styles/style-sera.css @@ -39,6 +39,20 @@ @apply text-xs/relaxed text-muted-foreground; } + /* Copyable — a value surface + copy button. Sharp, editorial: the button is a + bordered square that inverts to a solid foreground block on copy. */ + .cn-copyable { + @apply gap-3 rounded-none border border-input bg-transparent px-4 py-2.5 text-foreground; + } + + .cn-copyable-content { + @apply truncate font-mono text-xs text-foreground; + } + + .cn-copyable-button { + @apply size-9 rounded-none border border-input bg-transparent text-foreground transition-colors hover:bg-foreground hover:text-background focus-visible:ring-2 focus-visible:ring-ring/30 data-copied:border-foreground data-copied:bg-foreground data-copied:text-background; + } + /* Radio stays circular; checkbox is a hard square. Selected fills with the foreground and knocks the glyph out in the background color. */ .cn-choicebox-item-indicator { diff --git a/registry/styles/style-vega.css b/registry/styles/style-vega.css index 4ef7507..e713f64 100644 --- a/registry/styles/style-vega.css +++ b/registry/styles/style-vega.css @@ -32,6 +32,19 @@ @apply text-xs/relaxed text-muted-foreground; } + /* Copyable — a value surface + copy button. Bordered card, medium radius. */ + .cn-copyable { + @apply gap-2 rounded-xl border border-input bg-card px-3 py-2 text-card-foreground shadow-xs; + } + + .cn-copyable-content { + @apply truncate font-mono text-sm text-foreground; + } + + .cn-copyable-button { + @apply size-8 rounded-lg bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/30 data-copied:text-primary; + } + .cn-choicebox-item-indicator { @apply size-5 rounded-full data-[multiple]:rounded-sm border border-input text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground; } diff --git a/scripts/build-registry.ts b/scripts/build-registry.ts index f2cc629..dc82925 100644 --- a/scripts/build-registry.ts +++ b/scripts/build-registry.ts @@ -61,6 +61,11 @@ const CHOICEBOX_BASE_DESCRIPTION = const CHOICEBOX_RADIX_DESCRIPTION = "The Radix variant of Choicebox, built on the Radix Toggle Group (type=\"single\" | \"multiple\")."; +const COPYABLE_BASE_DESCRIPTION = + "A card-styled surface that displays a value alongside a button that copies it to the clipboard."; +const COPYABLE_RADIX_DESCRIPTION = + "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency)."; + const BASES: Base[] = [ { name: "base", @@ -74,6 +79,15 @@ const BASES: Base[] = [ file: "ui/choicebox.tsx", target: "components/ui/choicebox.tsx", }, + { + name: "copyable", + type: "registry:ui", + title: "Copyable", + description: COPYABLE_BASE_DESCRIPTION, + dependencies: [], + file: "ui/copyable.tsx", + target: "components/ui/copyable.tsx", + }, ], }, { @@ -88,6 +102,15 @@ const BASES: Base[] = [ file: "ui/choicebox.tsx", target: "components/ui/choicebox.tsx", }, + { + name: "copyable", + type: "registry:ui", + title: "Copyable (Radix)", + description: COPYABLE_RADIX_DESCRIPTION, + dependencies: [], + file: "ui/copyable.tsx", + target: "components/ui/copyable.tsx", + }, ], }, ];