Skip to content
edmi-ui

Snippet

One-line command or value with a copy button.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
$
id
$
id
$
id

Built on input-group.

Terminal window
npx shadcn@latest add @edmi-ui/ai-snippet
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/ai-snippet
Terminal window
yarn dlx shadcn@latest add @edmi-ui/ai-snippet
Terminal window
bunx shadcn@latest add @edmi-ui/ai-snippet

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/ai-snippet
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/ai-snippet
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/ai-snippet
Terminal window
bunx shadcn-vue@latest add @edmi-ui/ai-snippet

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-snippet.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-snippet.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-snippet.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-snippet.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

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>
registry/components/ai/snippet.tsx
"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>
);
};
registry/components/ai/snippet/Snippet.vue
<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>
registry/components/ai/snippet/SnippetAddon.vue
<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>
registry/components/ai/snippet/SnippetCopyButton.vue
<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>
registry/components/ai/snippet/SnippetInput.vue
<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>
registry/components/ai/snippet/SnippetText.vue
<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>
registry/components/ai/snippet/context.ts
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;
}
registry/components/ai/snippet/index.ts
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";
src/lib/registry/ai/snippet/index.ts
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 };
src/lib/registry/ai/snippet/snippet-addon.svelte
<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>
src/lib/registry/ai/snippet/snippet-copy-button.svelte
<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>
src/lib/registry/ai/snippet/snippet-input.svelte
<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>)}
/>
src/lib/registry/ai/snippet/snippet-text.svelte
<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>
src/lib/registry/ai/snippet/snippet.svelte
<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>
src/lib/registry/ai/snippet/use-snippet.svelte.ts
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
  • The input is read-only and mono at 12.5px; the prefix ($, id) sits in the muted addon.