Snippet
One-line command or value with a copy button.
import { Snippet, SnippetAddon, SnippetCopyButton, SnippetInput, SnippetText,} from "@/components/ai/snippet";
export default function Demo() { return ( <div className="flex w-full max-w-md flex-col gap-3"> <Snippet code="npx shadcn@latest add @edmi-ui/ai-tool"> <SnippetAddon> <SnippetText>$</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet> <Snippet code="idx_mag4_7f3a"> <SnippetAddon> <SnippetText>id</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet> </div> );}<script setup lang="ts">import { Snippet, SnippetAddon, SnippetCopyButton, SnippetInput, SnippetText } from "@/components/ai/snippet";</script>
<template> <div class="flex w-full max-w-md flex-col gap-3"> <Snippet code="npx shadcn-vue@latest add @edmi-ui/ai-tool"> <SnippetAddon> <SnippetText>$</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet> <Snippet code="idx_mag4_7f3a"> <SnippetAddon> <SnippetText>id</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet> </div></template><script lang="ts"> import { Snippet, SnippetAddon, SnippetCopyButton, SnippetInput, SnippetText, } from "$lib/components/ai/snippet";</script>
<div class="flex w-full max-w-md flex-col gap-3"> <Snippet code="npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-tool.json"> <SnippetAddon> <SnippetText>$</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet> <Snippet code="idx_mag4_7f3a"> <SnippetAddon> <SnippetText>id</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet></div>Built on input-group.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-snippetpnpm dlx shadcn@latest add @edmi-ui/ai-snippetyarn dlx shadcn@latest add @edmi-ui/ai-snippetbunx shadcn@latest add @edmi-ui/ai-snippetPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
input-group.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-snippetpnpm dlx shadcn-vue@latest add @edmi-ui/ai-snippetyarn dlx shadcn-vue@latest add @edmi-ui/ai-snippetbunx shadcn-vue@latest add @edmi-ui/ai-snippetPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @lucide/vueTerminal window pnpm add @lucide/vueTerminal window yarn add @lucide/vueTerminal window bun add @lucide/vueAdd the Edmi components this one builds on:
input-group.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-snippet.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-snippet.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-snippet.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-snippet.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
input-group.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Snippet, SnippetAddon, SnippetCopyButton, SnippetInput, SnippetText,} from "@/components/ai/snippet";
<div className="flex w-full max-w-md flex-col gap-3"> <Snippet code="npx shadcn@latest add @edmi-ui/ai-tool"> <SnippetAddon> <SnippetText>$</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet> <Snippet code="idx_mag4_7f3a"> <SnippetAddon> <SnippetText>id</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet></div><script setup lang="ts">import { Snippet, SnippetAddon, SnippetCopyButton, SnippetInput, SnippetText,} from "@/components/ai/snippet";</script>
<template> <div class="flex w-full max-w-md flex-col gap-3"> <Snippet code="npx shadcn-vue@latest add @edmi-ui/ai-tool"> <SnippetAddon> <SnippetText>$</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet> <Snippet code="idx_mag4_7f3a"> <SnippetAddon> <SnippetText>id</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet> </div></template><script lang="ts"> import { Snippet, SnippetAddon, SnippetCopyButton, SnippetInput, SnippetText, } from "$lib/components/ai/snippet";</script>
<div class="flex w-full max-w-md flex-col gap-3"> <Snippet code="npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-tool.json"> <SnippetAddon> <SnippetText>$</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet> <Snippet code="idx_mag4_7f3a"> <SnippetAddon> <SnippetText>id</SnippetText> </SnippetAddon> <SnippetInput /> <SnippetAddon align="inline-end"> <SnippetCopyButton /> </SnippetAddon> </Snippet></div>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { ComponentProps } from "react";import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState,} from "react";import { CheckIcon, CopyIcon } from "@phosphor-icons/react";import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText,} from "@/registry/edmi/ui/input-group";
interface SnippetContextType { code: string;}
const SnippetContext = createContext<SnippetContextType>({ code: "",});
export type SnippetProps = ComponentProps<typeof InputGroup> & { code: string;};
export const Snippet = ({ code, className, children, ...props}: SnippetProps) => { const contextValue = useMemo(() => ({ code }), [code]);
return ( <SnippetContext.Provider value={contextValue}> <InputGroup data-slot="ai-snippet" className={cn("font-mono", className)} {...props} > {children} </InputGroup> </SnippetContext.Provider> );};
export type SnippetAddonProps = ComponentProps<typeof InputGroupAddon>;
export const SnippetAddon = (props: SnippetAddonProps) => ( <InputGroupAddon {...props} />);
export type SnippetTextProps = ComponentProps<typeof InputGroupText>;
export const SnippetText = ({ className, ...props }: SnippetTextProps) => ( <InputGroupText className={cn("pl-2 font-mono text-xs font-normal", className)} {...props} />);
export type SnippetInputProps = Omit< ComponentProps<typeof InputGroupInput>, "readOnly" | "value">;
export const SnippetInput = ({ className, ...props }: SnippetInputProps) => { const { code } = useContext(SnippetContext);
return ( <InputGroupInput className={cn( "font-mono text-xs text-ellipsis text-foreground", className, )} readOnly value={code} {...props} /> );};
export type SnippetCopyButtonProps = ComponentProps<typeof InputGroupButton> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number;};
export const SnippetCopyButton = ({ onCopy, onError, timeout = 2000, children, className, ...props}: SnippetCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const timeoutRef = useRef<number>(0); const { code } = useContext(SnippetContext);
const copyToClipboard = useCallback(async () => { if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { onError?.(new Error("Clipboard API not available")); return; }
try { if (!isCopied) { await navigator.clipboard.writeText(code); setIsCopied(true); onCopy?.(); timeoutRef.current = window.setTimeout( () => setIsCopied(false), timeout, ); } } catch (error) { onError?.(error as Error); } }, [code, onCopy, onError, timeout, isCopied]);
useEffect( () => () => { window.clearTimeout(timeoutRef.current); }, [], );
return ( <InputGroupButton aria-label="Copy" className={cn("mr-1", className)} onClick={copyToClipboard} size="icon-xs" title="Copy" {...props} > {children ?? (isCopied ? ( <CheckIcon className="size-3.5" /> ) : ( <CopyIcon className="size-3.5" /> ))} </InputGroupButton> );};<script setup lang="ts">import type { HTMLAttributes } from "vue"import { computed, provide } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { InputGroup } from "@/registry/edmi/ui/input-group"import { SnippetKey } from "./context"
const props = defineProps<{ code: string class?: HTMLAttributes["class"]}>()
provide(SnippetKey, { code: computed(() => props.code),})</script>
<template> <InputGroup data-slot="ai-snippet" :class="cn('font-mono', props.class)"> <slot /> </InputGroup></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import type { InputGroupVariants } from "@/registry/edmi/ui/input-group"import { InputGroupAddon } from "@/registry/edmi/ui/input-group"
const props = defineProps<{ align?: InputGroupVariants["align"] class?: HTMLAttributes["class"]}>()</script>
<template> <InputGroupAddon :align="props.align" :class="props.class"> <slot /> </InputGroupAddon></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { CheckIcon, CopyIcon } from "@lucide/vue"import { onBeforeUnmount, ref } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { InputGroupButton } from "@/registry/edmi/ui/input-group"import { useSnippetContext } from "./context"
const props = withDefaults(defineProps<{ timeout?: number class?: HTMLAttributes["class"]}>(), { timeout: 2000,})
const emit = defineEmits<{ (e: "copy"): void (e: "error", error: Error): void}>()
const { code } = useSnippetContext("SnippetCopyButton")const isCopied = ref(false)let timer: ReturnType<typeof setTimeout> | undefined
async function copyToClipboard() { if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { emit("error", new Error("Clipboard API not available")) return } if (isCopied.value) return
try { await navigator.clipboard.writeText(code.value) isCopied.value = true emit("copy") timer = setTimeout(() => { isCopied.value = false }, props.timeout) } catch (error) { emit("error", error as Error) }}
onBeforeUnmount(() => clearTimeout(timer))</script>
<template> <InputGroupButton aria-label="Copy" title="Copy" size="icon-xs" :class="cn('mr-1', props.class)" @click="copyToClipboard" > <slot> <CheckIcon v-if="isCopied" class="size-3.5" /> <CopyIcon v-else class="size-3.5" /> </slot> </InputGroupButton></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { InputGroupInput } from "@/registry/edmi/ui/input-group"import { useSnippetContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { code } = useSnippetContext("SnippetInput")</script>
<template> <InputGroupInput :class="cn('font-mono text-xs text-ellipsis text-foreground', props.class)" readonly :model-value="code" /></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { InputGroupText } from "@/registry/edmi/ui/input-group"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <InputGroupText :class="cn('pl-2 font-mono text-xs font-normal', props.class)"> <slot /> </InputGroupText></template>import type { ComputedRef, InjectionKey } from "vue";import { inject } from "vue";
export interface SnippetContextValue { code: ComputedRef<string>;}
export const SnippetKey: InjectionKey<SnippetContextValue> = Symbol("SnippetContext");
export function useSnippetContext(componentName: string): SnippetContextValue { const context = inject(SnippetKey); if (!context) { throw new Error(`${componentName} must be used within Snippet`); } return context;}export { default as Snippet } from "./Snippet.vue";export { default as SnippetAddon } from "./SnippetAddon.vue";export { default as SnippetCopyButton } from "./SnippetCopyButton.vue";export { default as SnippetInput } from "./SnippetInput.vue";export { default as SnippetText } from "./SnippetText.vue";import Snippet from "./snippet.svelte";import SnippetAddon from "./snippet-addon.svelte";import SnippetCopyButton from "./snippet-copy-button.svelte";import SnippetInput from "./snippet-input.svelte";import SnippetText from "./snippet-text.svelte";
export { Snippet, SnippetAddon, SnippetCopyButton, SnippetInput, SnippetText };<script lang="ts"> import * as InputGroup from "$lib/registry/ui/input-group/index.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), children, ...restProps }: ComponentProps<typeof InputGroup.Addon> = $props();</script>
<InputGroup.Addon bind:ref {...restProps}> {@render children?.()}</InputGroup.Addon><script lang="ts"> import CheckIcon from "phosphor-svelte/lib/Check"; import CopyIcon from "phosphor-svelte/lib/Copy"; import * as InputGroup from "$lib/registry/ui/input-group/index.js"; import { cn } from "$lib/utils.js"; import { onDestroy, type ComponentProps } from "svelte"; import { useSnippetContext } from "./use-snippet.svelte.js";
let { onCopy, onError, timeout = 2000, class: className, children, ...restProps }: Omit<ComponentProps<typeof InputGroup.Button>, "onclick"> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number; } = $props();
const snippet = useSnippetContext(); let isCopied = $state(false); let timer: ReturnType<typeof setTimeout> | undefined;
async function copy() { if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { onError?.(new Error("Clipboard API not available")); return; } if (isCopied) return; try { await navigator.clipboard.writeText(snippet.code); isCopied = true; onCopy?.(); timer = setTimeout(() => (isCopied = false), timeout); } catch (error) { onError?.(error as Error); } }
onDestroy(() => clearTimeout(timer));</script>
<InputGroup.Button aria-label="Copy" title="Copy" size="icon-xs" class={cn("mr-1", className)} onclick={copy} {...restProps}> {#if children} {@render children()} {:else if isCopied} <CheckIcon class="size-3.5" /> {:else} <CopyIcon class="size-3.5" /> {/if}</InputGroup.Button><script lang="ts"> import * as InputGroup from "$lib/registry/ui/input-group/index.js"; import { cn } from "$lib/utils.js"; import type { HTMLInputAttributes } from "svelte/elements"; import { useSnippetContext } from "./use-snippet.svelte.js";
let { ref = $bindable(null), class: className, ...restProps }: Omit<HTMLInputAttributes, "value" | "readonly" | "type"> & { ref?: HTMLElement | null } = $props();
const snippet = useSnippetContext();</script>
<InputGroup.Input bind:ref class={cn("font-mono text-xs text-ellipsis text-foreground", className)} readonly value={snippet.code} {...(restProps as Record<string, unknown>)}/><script lang="ts"> import * as InputGroup from "$lib/registry/ui/input-group/index.js"; import { cn } from "$lib/utils.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, children, ...restProps }: ComponentProps<typeof InputGroup.Text> = $props();</script>
<InputGroup.Text bind:ref class={cn("pl-2 font-mono text-xs font-normal", className)} {...restProps}> {@render children?.()}</InputGroup.Text><script lang="ts"> import * as InputGroup from "$lib/registry/ui/input-group/index.js"; import { cn } from "$lib/utils.js"; import type { ComponentProps } from "svelte"; import { setSnippetContext } from "./use-snippet.svelte.js";
let { ref = $bindable(null), code, class: className, children, ...restProps }: ComponentProps<typeof InputGroup.Root> & { code: string } = $props();
setSnippetContext({ get code() { return code; }, });</script>
<InputGroup.Root bind:ref data-slot="ai-snippet" class={cn("font-mono", className)} {...restProps}> {@render children?.()}</InputGroup.Root>import { getContext, setContext } from "svelte";
const KEY = Symbol("ai-snippet");
export interface SnippetContext { readonly code: string;}
export function setSnippetContext(ctx: SnippetContext) { setContext(KEY, ctx);}
export function useSnippetContext(): SnippetContext { const ctx = getContext<SnippetContext | undefined>(KEY); if (!ctx) throw new Error("Snippet components must be used within Snippet"); return ctx;}| Part | Prop | Type | Default |
|---|---|---|---|
| Snippet | code |
string |
|
| SnippetAddon | align |
inline-start | inline-end |
inline-start |
| SnippetCopyButton | timeout, onCopy, onError |
number, () => void, (error) => void |
2000 |
| SnippetText, SnippetInput |
✦ Edmi additions
Section titled “✦ Edmi additions”- The input is read-only and mono at 12.5px; the prefix (
$,id) sits in the muted addon.