Skip to content
edmi-ui

Open In Chat

Dropdown that opens a prompt in ChatGPT, Claude, v0 or Cursor; T3, Scira and GitHub items are available too.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Built on button, dropdown-menu.

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

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-open-in-chat
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/ai-open-in-chat
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/ai-open-in-chat
Terminal window
bunx shadcn-vue@latest add @edmi-ui/ai-open-in-chat

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-open-in-chat.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-open-in-chat.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-open-in-chat.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-open-in-chat.json

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

import {
OpenIn,
OpenInChatGPT,
OpenInClaude,
OpenInContent,
OpenInCursor,
OpenInLabel,
OpenInSeparator,
OpenInTrigger,
OpenInv0,
} from "@/components/ai/open-in-chat";
<OpenIn query="Rebalance MAG4 so NVDAx is back at its 32% target">
<OpenInTrigger />
<OpenInContent>
<OpenInLabel>Open in chat</OpenInLabel>
<OpenInSeparator />
<OpenInClaude />
<OpenInChatGPT />
<OpenInv0 />
<OpenInCursor />
</OpenInContent>
</OpenIn>
<script setup lang="ts">
import {
OpenIn,
OpenInChatGPT,
OpenInClaude,
OpenInContent,
OpenInCursor,
OpenInLabel,
OpenInSeparator,
OpenInTrigger,
OpenInv0,
} from "@/components/ai/open-in-chat";
</script>
<template>
<OpenIn query="Rebalance MAG4 so NVDAx is back at its 32% target">
<OpenInTrigger />
<OpenInContent>
<OpenInLabel>Open in chat</OpenInLabel>
<OpenInSeparator />
<OpenInClaude />
<OpenInChatGPT />
<OpenInv0 />
<OpenInCursor />
</OpenInContent>
</OpenIn>
</template>
<script lang="ts">
import {
OpenIn,
OpenInChatGPT,
OpenInClaude,
OpenInContent,
OpenInCursor,
OpenInLabel,
OpenInSeparator,
OpenInTrigger,
OpenInv0,
} from "$lib/components/ai/open-in-chat";
</script>
<OpenIn query="Rebalance MAG4 so NVDAx is back at its 32% target">
<OpenInTrigger />
<OpenInContent>
<OpenInLabel>Open in chat</OpenInLabel>
<OpenInSeparator />
<OpenInClaude />
<OpenInChatGPT />
<OpenInv0 />
<OpenInCursor />
</OpenInContent>
</OpenIn>
registry/components/ai/open-in-chat.tsx
"use client";
import { cn } from "cn";
import type { ComponentProps } from "react";
import { createContext, useContext, useMemo } from "react";
import { ChatCircleIcon, CaretDownIcon, ArrowSquareOutIcon } from "@phosphor-icons/react";
import { Button } from "@/registry/edmi/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/edmi/ui/dropdown-menu";
const providers = {
chatgpt: {
color: "#10a37f",
createUrl: (prompt: string) =>
`https://chatgpt.com/?${new URLSearchParams({
hints: "search",
prompt,
})}`,
icon: (
<svg
fill="currentColor"
aria-hidden="true"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
),
title: "Open in ChatGPT",
},
claude: {
color: "#d97757",
createUrl: (q: string) =>
`https://claude.ai/new?${new URLSearchParams({
q,
})}`,
icon: (
<svg
fill="currentColor"
aria-hidden="true"
viewBox="0 0 12 12"
xmlns="http://www.w3.org/2000/svg"
>
<path
clipRule="evenodd"
d="M2.3545 7.9775L4.7145 6.654L4.7545 6.539L4.7145 6.475H4.6L4.205 6.451L2.856 6.4145L1.6865 6.366L0.5535 6.305L0.268 6.2445L0 5.892L0.0275 5.716L0.2675 5.5555L0.6105 5.5855L1.3705 5.637L2.5095 5.716L3.3355 5.7645L4.56 5.892H4.7545L4.782 5.8135L4.715 5.7645L4.6635 5.716L3.4845 4.918L2.2085 4.074L1.5405 3.588L1.1785 3.3425L0.9965 3.1115L0.9175 2.6075L1.2455 2.2465L1.686 2.2765L1.7985 2.307L2.245 2.65L3.199 3.388L4.4445 4.3045L4.627 4.4565L4.6995 4.405L4.709 4.3685L4.627 4.2315L3.9495 3.0085L3.2265 1.7635L2.9045 1.2475L2.8195 0.938C2.78711 0.819128 2.76965 0.696687 2.7675 0.5735L3.1415 0.067L3.348 0L3.846 0.067L4.056 0.249L4.366 0.956L4.867 2.0705L5.6445 3.5855L5.8725 4.0345L5.994 4.4505L6.0395 4.578H6.1185V4.505L6.1825 3.652L6.301 2.6045L6.416 1.257L6.456 0.877L6.644 0.422L7.0175 0.176L7.3095 0.316L7.5495 0.6585L7.516 0.8805L7.373 1.806L7.0935 3.2575L6.9115 4.2285H7.0175L7.139 4.1075L7.6315 3.4545L8.4575 2.4225L8.8225 2.0125L9.2475 1.5605L9.521 1.345H10.0375L10.4175 1.9095L10.2475 2.4925L9.7155 3.166L9.275 3.737L8.643 4.587L8.248 5.267L8.2845 5.322L8.3785 5.312L9.8065 5.009L10.578 4.869L11.4985 4.7115L11.915 4.9055L11.9605 5.103L11.7965 5.5065L10.812 5.7495L9.6575 5.9805L7.938 6.387L7.917 6.402L7.9415 6.4325L8.716 6.5055L9.047 6.5235H9.858L11.368 6.636L11.763 6.897L12 7.216L11.9605 7.4585L11.353 7.7685L10.533 7.574L8.6185 7.119L7.9625 6.9545H7.8715V7.0095L8.418 7.5435L9.421 8.4485L10.6755 9.6135L10.739 9.9025L10.578 10.13L10.408 10.1055L9.3055 9.277L8.88 8.9035L7.917 8.0935H7.853V8.1785L8.075 8.503L9.2475 10.2635L9.3085 10.8035L9.2235 10.98L8.9195 11.0865L8.5855 11.0255L7.8985 10.063L7.191 8.9795L6.6195 8.008L6.5495 8.048L6.2125 11.675L6.0545 11.86L5.69 12L5.3865 11.7695L5.2255 11.396L5.3865 10.658L5.581 9.696L5.7385 8.931L5.8815 7.981L5.9665 7.665L5.9605 7.644L5.8905 7.653L5.1735 8.6365L4.0835 10.109L3.2205 11.0315L3.0135 11.1135L2.655 10.9285L2.6885 10.5975L2.889 10.303L4.083 8.785L4.803 7.844L5.268 7.301L5.265 7.222H5.2375L2.066 9.28L1.501 9.353L1.2575 9.125L1.288 8.752L1.4035 8.6305L2.3575 7.9745L2.3545 7.9775Z"
fillRule="evenodd"
/>
</svg>
),
title: "Open in Claude",
},
cursor: {
color: "#333",
createUrl: (text: string) => {
const url = new URL("https://cursor.com/link/prompt");
url.searchParams.set("text", text);
return url.toString();
},
icon: (
<svg
aria-hidden="true"
version="1.1"
viewBox="0 0 466.73 532.09"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"
fill="currentColor"
/>
</svg>
),
title: "Open in Cursor",
},
github: {
color: "#24292f",
createUrl: (url: string) => url,
icon: (
<svg fill="currentColor" aria-hidden="true" viewBox="0 0 24 24">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
),
title: "Open in GitHub",
},
scira: {
color: "#111",
createUrl: (q: string) =>
`https://scira.ai/?${new URLSearchParams({
q,
})}`,
icon: (
<svg
aria-hidden="true"
fill="none"
height="934"
viewBox="0 0 910 934"
width="910"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="20"
/>
<path
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="20"
/>
<path
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="30"
/>
</svg>
),
title: "Open in Scira",
},
t3: {
color: "#8b1d6b",
createUrl: (q: string) =>
`https://t3.chat/new?${new URLSearchParams({
q,
})}`,
icon: (
<ChatCircleIcon
/>
),
title: "Open in T3 Chat",
},
v0: {
color: "#111",
createUrl: (q: string) =>
`https://v0.app?${new URLSearchParams({
q,
})}`,
icon: (
<svg
aria-hidden="true"
fill="currentColor"
viewBox="0 0 147 70"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M56 50.2031V14H70V60.1562C70 65.5928 65.5928 70 60.1562 70C57.5605 70 54.9982 68.9992 53.1562 67.1573L0 14H19.7969L56 50.2031Z" />
<path d="M147 56H133V23.9531L100.953 56H133V70H96.6875C85.8144 70 77 61.1856 77 50.3125V14H91V46.1562L123.156 14H91V0H127.312C138.186 0 147 8.81439 147 19.6875V56Z" />
</svg>
),
title: "Open in v0",
},
};
const OpenInContext = createContext<{ query: string } | undefined>(undefined);
const useOpenInContext = () => {
const context = useContext(OpenInContext);
if (!context) {
throw new Error("OpenIn components must be used within an OpenIn provider");
}
return context;
};
export type OpenInProps = ComponentProps<typeof DropdownMenu> & {
query: string;
};
/** Dropdown that sends `query` to another AI app. */
export const OpenIn = ({ query, ...props }: OpenInProps) => {
const contextValue = useMemo(() => ({ query }), [query]);
return (
<OpenInContext.Provider value={contextValue}>
<DropdownMenu {...props} />
</OpenInContext.Provider>
);
};
export type OpenInContentProps = ComponentProps<typeof DropdownMenuContent>;
export const OpenInContent = ({
className,
children,
...props
}: OpenInContentProps) => (
<DropdownMenuContent
align="start"
className={cn("w-[230px]", className)}
data-slot="ai-open-in-content"
{...props}
>
{/* Base UI requires menu labels to live inside a group. */}
<DropdownMenuGroup>{children}</DropdownMenuGroup>
</DropdownMenuContent>
);
export type OpenInItemProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInItem = (props: OpenInItemProps) => (
<DropdownMenuItem {...props} />
);
export type OpenInLabelProps = ComponentProps<typeof DropdownMenuLabel>;
export const OpenInLabel = (props: OpenInLabelProps) => (
<DropdownMenuLabel {...props} />
);
export type OpenInSeparatorProps = ComponentProps<typeof DropdownMenuSeparator>;
export const OpenInSeparator = (props: OpenInSeparatorProps) => (
<DropdownMenuSeparator {...props} />
);
export type OpenInTriggerProps = ComponentProps<typeof DropdownMenuTrigger>;
export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => (
<DropdownMenuTrigger
data-slot="ai-open-in-trigger"
render={<Button size="sm" type="button" variant="outline" />}
{...props}
>
{children ?? (
<>
Open in
<CaretDownIcon
className="size-3.5"
/>
</>
)}
</DropdownMenuTrigger>
);
type ProviderKey = keyof typeof providers;
const ProviderItem = ({
provider,
className,
...props
}: ComponentProps<typeof DropdownMenuItem> & { provider: ProviderKey }) => {
const { query } = useOpenInContext();
const { color, createUrl, icon, title } = providers[provider];
return (
<DropdownMenuItem
className={cn("[&_svg]:text-current", className)}
render={
<a href={createUrl(query)} rel="noopener noreferrer" target="_blank" />
}
{...props}
>
<span
className="inline-flex size-[18px] shrink-0 items-center justify-center rounded-[5px] text-white [&_svg]:size-3"
style={{ background: color }}
>
{icon}
</span>
<span className="flex-1">{title.replace("Open in ", "")}</span>
<ArrowSquareOutIcon
className="size-[13px] shrink-0 text-muted-foreground"
/>
</DropdownMenuItem>
);
};
export type OpenInChatGPTProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInChatGPT = (props: OpenInChatGPTProps) => (
<ProviderItem provider="chatgpt" {...props} />
);
export type OpenInClaudeProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInClaude = (props: OpenInClaudeProps) => (
<ProviderItem provider="claude" {...props} />
);
export type OpenInT3Props = ComponentProps<typeof DropdownMenuItem>;
export const OpenInT3 = (props: OpenInT3Props) => (
<ProviderItem provider="t3" {...props} />
);
export type OpenInSciraProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInScira = (props: OpenInSciraProps) => (
<ProviderItem provider="scira" {...props} />
);
export type OpenInv0Props = ComponentProps<typeof DropdownMenuItem>;
export const OpenInv0 = (props: OpenInv0Props) => (
<ProviderItem provider="v0" {...props} />
);
export type OpenInCursorProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInCursor = (props: OpenInCursorProps) => (
<ProviderItem provider="cursor" {...props} />
);
export type OpenInGitHubProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInGitHub = (props: OpenInGitHubProps) => (
<ProviderItem provider="github" {...props} />
);
registry/components/ai/open-in-chat/OpenIn.vue
<script setup lang="ts">
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui"
import { reactiveOmit } from "@vueuse/core"
import { useForwardPropsEmits } from "reka-ui"
import { DropdownMenu } from "@/registry/edmi/ui/dropdown-menu"
import { provideOpenInContext } from "./context"
/** Dropdown that sends `query` to another AI app. */
const props = defineProps<DropdownMenuRootProps & { query: string }>()
const emits = defineEmits<DropdownMenuRootEmits>()
provideOpenInContext({
get query() {
return props.query
},
})
const delegatedProps = reactiveOmit(props, "query")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenu v-bind="forwarded">
<slot />
</DropdownMenu>
</template>
registry/components/ai/open-in-chat/OpenInChatGPT.vue
<script setup lang="ts">
import OpenInItemLink from "./OpenInItemLink.vue"
</script>
<template>
<OpenInItemLink provider="chatgpt" v-bind="$attrs" />
</template>
registry/components/ai/open-in-chat/OpenInClaude.vue
<script setup lang="ts">
import OpenInItemLink from "./OpenInItemLink.vue"
</script>
<template>
<OpenInItemLink provider="claude" v-bind="$attrs" />
</template>
registry/components/ai/open-in-chat/OpenInContent.vue
<script setup lang="ts">
import type { DropdownMenuContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { useForwardProps } from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
import { DropdownMenuContent } from "@/registry/edmi/ui/dropdown-menu"
const props = withDefaults(defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(), {
align: "start",
})
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuContent data-slot="ai-open-in-content" v-bind="forwarded" :class="cn('w-[230px]', props.class)">
<slot />
</DropdownMenuContent>
</template>
registry/components/ai/open-in-chat/OpenInCursor.vue
<script setup lang="ts">
import OpenInItemLink from "./OpenInItemLink.vue"
</script>
<template>
<OpenInItemLink provider="cursor" v-bind="$attrs" />
</template>
registry/components/ai/open-in-chat/OpenInGitHub.vue
<script setup lang="ts">
import OpenInItemLink from "./OpenInItemLink.vue"
</script>
<template>
<OpenInItemLink provider="github" v-bind="$attrs" />
</template>
registry/components/ai/open-in-chat/OpenInItem.vue
<script setup lang="ts">
import { DropdownMenuItem } from "@/registry/edmi/ui/dropdown-menu"
</script>
<template>
<DropdownMenuItem v-bind="$attrs">
<slot />
</DropdownMenuItem>
</template>
registry/components/ai/open-in-chat/OpenInItemLink.vue
<script setup lang="ts">
import type { ProviderKey } from "./providers"
import { ArrowUpRightIcon } from "@lucide/vue"
import { computed } from "vue"
import { DropdownMenuItem } from "@/registry/edmi/ui/dropdown-menu"
import { useOpenInContext } from "./context"
import { providers } from "./providers"
const props = defineProps<{ provider: ProviderKey }>()
const context = useOpenInContext()
const provider = computed(() => providers[props.provider])
const href = computed(() => provider.value.createUrl(context.query))
const label = computed(() => provider.value.title.replace("Open in ", ""))
</script>
<template>
<DropdownMenuItem v-bind="$attrs" as-child>
<a :href="href" rel="noopener noreferrer" target="_blank">
<span
class="inline-flex size-[18px] shrink-0 items-center justify-center rounded-[5px] text-white [&_svg]:size-3"
:style="{ background: provider.color }"
>
<component :is="provider.icon" />
</span>
<span class="flex-1">{{ label }}</span>
<ArrowUpRightIcon class="size-[13px] shrink-0 text-muted-foreground" />
</a>
</DropdownMenuItem>
</template>
registry/components/ai/open-in-chat/OpenInLabel.vue
<script setup lang="ts">
import { DropdownMenuLabel } from "@/registry/edmi/ui/dropdown-menu"
</script>
<template>
<DropdownMenuLabel v-bind="$attrs">
<slot />
</DropdownMenuLabel>
</template>
registry/components/ai/open-in-chat/OpenInScira.vue
<script setup lang="ts">
import OpenInItemLink from "./OpenInItemLink.vue"
</script>
<template>
<OpenInItemLink provider="scira" v-bind="$attrs" />
</template>
registry/components/ai/open-in-chat/OpenInSeparator.vue
<script setup lang="ts">
import { DropdownMenuSeparator } from "@/registry/edmi/ui/dropdown-menu"
</script>
<template>
<DropdownMenuSeparator v-bind="$attrs">
<slot />
</DropdownMenuSeparator>
</template>
registry/components/ai/open-in-chat/OpenInT3.vue
<script setup lang="ts">
import OpenInItemLink from "./OpenInItemLink.vue"
</script>
<template>
<OpenInItemLink provider="t3" v-bind="$attrs" />
</template>
registry/components/ai/open-in-chat/OpenInTrigger.vue
<script setup lang="ts">
import { ChevronDownIcon } from "@lucide/vue"
import { Button } from "@/registry/edmi/ui/button"
import { DropdownMenuTrigger } from "@/registry/edmi/ui/dropdown-menu"
</script>
<template>
<DropdownMenuTrigger as-child data-slot="ai-open-in-trigger">
<slot>
<Button type="button" variant="outline" size="sm">
Open in
<ChevronDownIcon class="size-3.5" />
</Button>
</slot>
</DropdownMenuTrigger>
</template>
registry/components/ai/open-in-chat/OpenInv0.vue
<script setup lang="ts">
import OpenInItemLink from "./OpenInItemLink.vue"
</script>
<template>
<OpenInItemLink provider="v0" v-bind="$attrs" />
</template>
registry/components/ai/open-in-chat/context.ts
import type { InjectionKey } from "vue";
import { inject, provide } from "vue";
export interface OpenInContext {
readonly query: string;
}
export const OpenInContextKey: InjectionKey<OpenInContext> =
Symbol("OpenInContext");
export function provideOpenInContext(context: OpenInContext) {
provide(OpenInContextKey, context);
}
export function useOpenInContext() {
const context = inject(OpenInContextKey);
if (!context) {
throw new Error("OpenIn components must be used within an OpenIn provider");
}
return context;
}
registry/components/ai/open-in-chat/providers.ts
import { MessageSquareIcon } from "@lucide/vue";
import type { Component } from "vue";
import {
ChatGPTIcon,
ClaudeIcon,
CursorIcon,
GithubIcon,
SciraIcon,
V0Icon,
} from "./icons";
/** Brand tile colors follow the AI 08 board (brand marks are the one place literal colors are allowed). */
export const providers = {
github: {
title: "Open in GitHub",
color: "#24292f",
createUrl: (url: string) => url,
icon: GithubIcon as Component,
},
scira: {
title: "Open in Scira",
color: "#111",
createUrl: (q: string) => `https://scira.ai/?${new URLSearchParams({ q })}`,
icon: SciraIcon as Component,
},
chatgpt: {
title: "Open in ChatGPT",
color: "#10a37f",
createUrl: (prompt: string) =>
`https://chatgpt.com/?${new URLSearchParams({ hints: "search", prompt })}`,
icon: ChatGPTIcon as Component,
},
claude: {
title: "Open in Claude",
color: "#d97757",
createUrl: (q: string) =>
`https://claude.ai/new?${new URLSearchParams({ q })}`,
icon: ClaudeIcon as Component,
},
t3: {
title: "Open in T3 Chat",
color: "#8b1d6b",
createUrl: (q: string) =>
`https://t3.chat/new?${new URLSearchParams({ q })}`,
icon: MessageSquareIcon as Component,
},
v0: {
title: "Open in v0",
color: "#111",
createUrl: (q: string) => `https://v0.app?${new URLSearchParams({ q })}`,
icon: V0Icon as Component,
},
cursor: {
title: "Open in Cursor",
color: "#333",
createUrl: (text: string) => {
const url = new URL("https://cursor.com/link/prompt");
url.searchParams.set("text", text);
return url.toString();
},
icon: CursorIcon as Component,
},
};
export type ProviderKey = keyof typeof providers;
registry/components/ai/open-in-chat/icons/ChatGPT.vue
<template>
<svg
fill="currentColor"
aria-hidden="true"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
/>
</svg>
</template>
registry/components/ai/open-in-chat/icons/Claude.vue
<template>
<svg
fill="currentColor"
aria-hidden="true"
viewBox="0 0 12 12"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M2.3545 7.9775L4.7145 6.654L4.7545 6.539L4.7145 6.475H4.6L4.205 6.451L2.856 6.4145L1.6865 6.366L0.5535 6.305L0.268 6.2445L0 5.892L0.0275 5.716L0.2675 5.5555L0.6105 5.5855L1.3705 5.637L2.5095 5.716L3.3355 5.7645L4.56 5.892H4.7545L4.782 5.8135L4.715 5.7645L4.6635 5.716L3.4845 4.918L2.2085 4.074L1.5405 3.588L1.1785 3.3425L0.9965 3.1115L0.9175 2.6075L1.2455 2.2465L1.686 2.2765L1.7985 2.307L2.245 2.65L3.199 3.388L4.4445 4.3045L4.627 4.4565L4.6995 4.405L4.709 4.3685L4.627 4.2315L3.9495 3.0085L3.2265 1.7635L2.9045 1.2475L2.8195 0.938C2.78711 0.819128 2.76965 0.696687 2.7675 0.5735L3.1415 0.067L3.348 0L3.846 0.067L4.056 0.249L4.366 0.956L4.867 2.0705L5.6445 3.5855L5.8725 4.0345L5.994 4.4505L6.0395 4.578H6.1185V4.505L6.1825 3.652L6.301 2.6045L6.416 1.257L6.456 0.877L6.644 0.422L7.0175 0.176L7.3095 0.316L7.5495 0.6585L7.516 0.8805L7.373 1.806L7.0935 3.2575L6.9115 4.2285H7.0175L7.139 4.1075L7.6315 3.4545L8.4575 2.4225L8.8225 2.0125L9.2475 1.5605L9.521 1.345H10.0375L10.4175 1.9095L10.2475 2.4925L9.7155 3.166L9.275 3.737L8.643 4.587L8.248 5.267L8.2845 5.322L8.3785 5.312L9.8065 5.009L10.578 4.869L11.4985 4.7115L11.915 4.9055L11.9605 5.103L11.7965 5.5065L10.812 5.7495L9.6575 5.9805L7.938 6.387L7.917 6.402L7.9415 6.4325L8.716 6.5055L9.047 6.5235H9.858L11.368 6.636L11.763 6.897L12 7.216L11.9605 7.4585L11.353 7.7685L10.533 7.574L8.6185 7.119L7.9625 6.9545H7.8715V7.0095L8.418 7.5435L9.421 8.4485L10.6755 9.6135L10.739 9.9025L10.578 10.13L10.408 10.1055L9.3055 9.277L8.88 8.9035L7.917 8.0935H7.853V8.1785L8.075 8.503L9.2475 10.2635L9.3085 10.8035L9.2235 10.98L8.9195 11.0865L8.5855 11.0255L7.8985 10.063L7.191 8.9795L6.6195 8.008L6.5495 8.048L6.2125 11.675L6.0545 11.86L5.69 12L5.3865 11.7695L5.2255 11.396L5.3865 10.658L5.581 9.696L5.7385 8.931L5.8815 7.981L5.9665 7.665L5.9605 7.644L5.8905 7.653L5.1735 8.6365L4.0835 10.109L3.2205 11.0315L3.0135 11.1135L2.655 10.9285L2.6885 10.5975L2.889 10.303L4.083 8.785L4.803 7.844L5.268 7.301L5.265 7.222H5.2375L2.066 9.28L1.501 9.353L1.2575 9.125L1.288 8.752L1.4035 8.6305L2.3575 7.9745L2.3545 7.9775Z"
fill-rule="evenodd"
/>
</svg>
</template>
registry/components/ai/open-in-chat/icons/Cursor.vue
<template>
<svg aria-hidden="true"
version="1.1"
viewBox="0 0 466.73 532.09"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"
fill="currentColor"
/>
</svg>
</template>
registry/components/ai/open-in-chat/icons/Github.vue
<template>
<svg fill="currentColor" aria-hidden="true" viewBox="0 0 24 24">
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/>
</svg>
</template>
registry/components/ai/open-in-chat/icons/Scira.vue
<template>
<svg aria-hidden="true"
fill="none"
height="934"
viewBox="0 0 910 934"
width="910"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
fill="currentColor"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="8"
/>
<path
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
fill="currentColor"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="8"
/>
<path
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="20"
/>
<path
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="20"
/>
<path
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
fill="currentColor"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="8"
/>
<path
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
fill="currentColor"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="8"
/>
<path
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="30"
/>
</svg>
</template>
registry/components/ai/open-in-chat/icons/V0.vue
<template>
<svg aria-hidden="true"
fill="currentColor"
viewBox="0 0 147 70"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M56 50.2031V14H70V60.1562C70 65.5928 65.5928 70 60.1562 70C57.5605 70 54.9982 68.9992 53.1562 67.1573L0 14H19.7969L56 50.2031Z"
/>
<path
d="M147 56H133V23.9531L100.953 56H133V70H96.6875C85.8144 70 77 61.1856 77 50.3125V14H91V46.1562L123.156 14H91V0H127.312C138.186 0 147 8.81439 147 19.6875V56Z"
/>
</svg>
</template>
registry/components/ai/open-in-chat/icons/index.ts
export { default as ChatGPTIcon } from "./ChatGPT.vue";
export { default as ClaudeIcon } from "./Claude.vue";
export { default as CursorIcon } from "./Cursor.vue";
export { default as GithubIcon } from "./Github.vue";
export { default as SciraIcon } from "./Scira.vue";
export { default as V0Icon } from "./V0.vue";
registry/components/ai/open-in-chat/index.ts
export { default as OpenIn } from "./OpenIn.vue";
export { default as OpenInChatGPT } from "./OpenInChatGPT.vue";
export { default as OpenInClaude } from "./OpenInClaude.vue";
export { default as OpenInContent } from "./OpenInContent.vue";
export { default as OpenInCursor } from "./OpenInCursor.vue";
export { default as OpenInGitHub } from "./OpenInGitHub.vue";
export { default as OpenInItem } from "./OpenInItem.vue";
export { default as OpenInLabel } from "./OpenInLabel.vue";
export { default as OpenInScira } from "./OpenInScira.vue";
export { default as OpenInSeparator } from "./OpenInSeparator.vue";
export { default as OpenInT3 } from "./OpenInT3.vue";
export { default as OpenInTrigger } from "./OpenInTrigger.vue";
export { default as OpenInv0 } from "./OpenInv0.vue";
src/lib/registry/ai/open-in-chat/open-in.svelte
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import * as DropdownMenu from "$lib/registry/ui/dropdown-menu/index.js";
import { setOpenInContext } from "./context.js";
/** Dropdown that sends `query` to another AI app. */
let {
open = $bindable(false),
query,
children,
...restProps
}: DropdownMenuPrimitive.RootProps & { query: string } = $props();
setOpenInContext({
get query() {
return query;
},
});
</script>
<DropdownMenu.Root bind:open {...restProps}>
{@render children?.()}
</DropdownMenu.Root>
src/lib/registry/ai/open-in-chat/open-in-chatgpt.svelte
<script lang="ts">
import type { ComponentProps } from "svelte";
import OpenInItemLink from "./open-in-item-link.svelte";
let { ...restProps }: Omit<ComponentProps<typeof OpenInItemLink>, "provider"> = $props();
</script>
<OpenInItemLink provider="chatgpt" {...restProps} />
src/lib/registry/ai/open-in-chat/open-in-claude.svelte
<script lang="ts">
import type { ComponentProps } from "svelte";
import OpenInItemLink from "./open-in-item-link.svelte";
let { ...restProps }: Omit<ComponentProps<typeof OpenInItemLink>, "provider"> = $props();
</script>
<OpenInItemLink provider="claude" {...restProps} />
src/lib/registry/ai/open-in-chat/open-in-content.svelte
<script lang="ts">
import * as DropdownMenu from "$lib/registry/ui/dropdown-menu/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
class: className,
align = "start",
children,
...restProps
}: ComponentProps<typeof DropdownMenu.Content> = $props();
</script>
<DropdownMenu.Content data-slot="ai-open-in-content" {align} class={cn("w-[230px]", className)} {...restProps}>
{@render children?.()}
</DropdownMenu.Content>
src/lib/registry/ai/open-in-chat/open-in-cursor.svelte
<script lang="ts">
import type { ComponentProps } from "svelte";
import OpenInItemLink from "./open-in-item-link.svelte";
let { ...restProps }: Omit<ComponentProps<typeof OpenInItemLink>, "provider"> = $props();
</script>
<OpenInItemLink provider="cursor" {...restProps} />
src/lib/registry/ai/open-in-chat/open-in-github.svelte
<script lang="ts">
import type { ComponentProps } from "svelte";
import OpenInItemLink from "./open-in-item-link.svelte";
let { ...restProps }: Omit<ComponentProps<typeof OpenInItemLink>, "provider"> = $props();
</script>
<OpenInItemLink provider="github" {...restProps} />
src/lib/registry/ai/open-in-chat/open-in-item-link.svelte
<script lang="ts">
import * as DropdownMenu from "$lib/registry/ui/dropdown-menu/index.js";
import ArrowSquareOutIcon from "phosphor-svelte/lib/ArrowSquareOut";
import type { ComponentProps } from "svelte";
import { getOpenInContext } from "./context.js";
import { type ProviderKey, providers } from "./providers.js";
let {
provider,
...restProps
}: Omit<ComponentProps<typeof DropdownMenu.Item>, "child" | "children"> & { provider: ProviderKey } = $props();
const context = getOpenInContext();
const config = $derived(providers[provider]);
const Icon = $derived(config.icon);
const href = $derived(config.createUrl(context.query));
const label = $derived(config.title.replace("Open in ", ""));
</script>
<DropdownMenu.Item {...restProps}>
{#snippet child({ props })}
<a {...props} {href} rel="noopener noreferrer" target="_blank">
<span
class="inline-flex size-[18px] shrink-0 items-center justify-center rounded-[5px] text-white [&_svg]:size-3"
style:background={config.color}
>
<Icon />
</span>
<span class="flex-1">{label}</span>
<ArrowSquareOutIcon
class="size-[13px] shrink-0 text-muted-foreground"
/>
</a>
{/snippet}
</DropdownMenu.Item>
src/lib/registry/ai/open-in-chat/open-in-item.svelte
<script lang="ts">
import * as DropdownMenu from "$lib/registry/ui/dropdown-menu/index.js";
import type { ComponentProps } from "svelte";
let { children, ...restProps }: ComponentProps<typeof DropdownMenu.Item> = $props();
</script>
<DropdownMenu.Item {...restProps}>
{@render children?.()}
</DropdownMenu.Item>
src/lib/registry/ai/open-in-chat/open-in-label.svelte
<script lang="ts">
import * as DropdownMenu from "$lib/registry/ui/dropdown-menu/index.js";
import type { ComponentProps } from "svelte";
let { children, ...restProps }: ComponentProps<typeof DropdownMenu.Label> = $props();
</script>
<DropdownMenu.Label {...restProps}>
{@render children?.()}
</DropdownMenu.Label>
src/lib/registry/ai/open-in-chat/open-in-scira.svelte
<script lang="ts">
import type { ComponentProps } from "svelte";
import OpenInItemLink from "./open-in-item-link.svelte";
let { ...restProps }: Omit<ComponentProps<typeof OpenInItemLink>, "provider"> = $props();
</script>
<OpenInItemLink provider="scira" {...restProps} />
src/lib/registry/ai/open-in-chat/open-in-separator.svelte
<script lang="ts">
import * as DropdownMenu from "$lib/registry/ui/dropdown-menu/index.js";
import type { ComponentProps } from "svelte";
let { children, ...restProps }: ComponentProps<typeof DropdownMenu.Separator> = $props();
</script>
<DropdownMenu.Separator {...restProps}>
{@render children?.()}
</DropdownMenu.Separator>
src/lib/registry/ai/open-in-chat/open-in-t3.svelte
<script lang="ts">
import type { ComponentProps } from "svelte";
import OpenInItemLink from "./open-in-item-link.svelte";
let { ...restProps }: Omit<ComponentProps<typeof OpenInItemLink>, "provider"> = $props();
</script>
<OpenInItemLink provider="t3" {...restProps} />
src/lib/registry/ai/open-in-chat/open-in-trigger.svelte
<script lang="ts">
import * as DropdownMenu from "$lib/registry/ui/dropdown-menu/index.js";
import CaretDownIcon from "phosphor-svelte/lib/CaretDown";
import { buttonVariants } from "$lib/registry/ui/button/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let { class: className, children, ...restProps }: ComponentProps<typeof DropdownMenu.Trigger> = $props();
</script>
<DropdownMenu.Trigger
data-slot="ai-open-in-trigger"
class={cn(buttonVariants({ variant: "outline", size: "sm" }), className)}
{...restProps}
>
{#if children}
{@render children()}
{:else}
Open in
<CaretDownIcon
class="size-3.5"
/>
{/if}
</DropdownMenu.Trigger>
src/lib/registry/ai/open-in-chat/open-in-v0.svelte
<script lang="ts">
import type { ComponentProps } from "svelte";
import OpenInItemLink from "./open-in-item-link.svelte";
let { ...restProps }: Omit<ComponentProps<typeof OpenInItemLink>, "provider"> = $props();
</script>
<OpenInItemLink provider="v0" {...restProps} />
src/lib/registry/ai/open-in-chat/context.ts
import { getContext, setContext } from "svelte";
export type OpenInContext = { readonly query: string };
const KEY = Symbol("open-in-context");
/** `context` is a getter object so a changing `query` prop stays reactive. */
export function setOpenInContext(context: OpenInContext) {
setContext(KEY, context);
}
export function getOpenInContext(): OpenInContext {
const context = getContext<OpenInContext>(KEY);
if (!context) {
throw new Error("OpenIn components must be used within an OpenIn provider");
}
return context;
}
src/lib/registry/ai/open-in-chat/providers.ts
import type { Component } from "svelte";
import ChatGPTIcon from "./icons/chatgpt.svelte";
import ClaudeIcon from "./icons/claude.svelte";
import CursorIcon from "./icons/cursor.svelte";
import GitHubIcon from "./icons/github.svelte";
import SciraIcon from "./icons/scira.svelte";
import T3Icon from "./icons/t3.svelte";
import V0Icon from "./icons/v0.svelte";
export type ProviderKey =
| "github"
| "scira"
| "chatgpt"
| "claude"
| "t3"
| "v0"
| "cursor";
export type ProviderConfig = {
title: string;
/** Brand tile color (AI 08 board; brand marks are the one place literal colors are allowed). */
color: string;
createUrl: (query: string) => string;
// biome-ignore lint/suspicious/noExplicitAny: icon components take different props
icon: Component<any>;
};
export const providers: Record<ProviderKey, ProviderConfig> = {
github: {
title: "Open in GitHub",
color: "#24292f",
createUrl: (url) => url,
icon: GitHubIcon,
},
scira: {
title: "Open in Scira",
color: "#111",
createUrl: (q) => `https://scira.ai/?${new URLSearchParams({ q })}`,
icon: SciraIcon,
},
chatgpt: {
title: "Open in ChatGPT",
color: "#10a37f",
createUrl: (prompt) =>
`https://chatgpt.com/?${new URLSearchParams({ hints: "search", prompt })}`,
icon: ChatGPTIcon,
},
claude: {
title: "Open in Claude",
color: "#d97757",
createUrl: (q) => `https://claude.ai/new?${new URLSearchParams({ q })}`,
icon: ClaudeIcon,
},
t3: {
title: "Open in T3 Chat",
color: "#8b1d6b",
createUrl: (q) => `https://t3.chat/new?${new URLSearchParams({ q })}`,
icon: T3Icon,
},
v0: {
title: "Open in v0",
color: "#111",
createUrl: (q) => `https://v0.app?${new URLSearchParams({ q })}`,
icon: V0Icon,
},
cursor: {
title: "Open in Cursor",
color: "#333",
createUrl: (text) => {
const url = new URL("https://cursor.com/link/prompt");
url.searchParams.set("text", text);
return url.toString();
},
icon: CursorIcon,
},
};
src/lib/registry/ai/open-in-chat/icons/chatgpt.svelte
<script lang="ts">
interface Props {
class?: string;
size?: string | number;
}
let { class: className = "", size = "1em", ...rest }: Props = $props();
</script>
<svg
fill="currentColor"
aria-hidden="true"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
class={className}
{...rest}
>
<path
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
/>
</svg>
src/lib/registry/ai/open-in-chat/icons/claude.svelte
<script lang="ts">
interface Props {
class?: string;
size?: string | number;
}
let { class: className = "", size = "1em", ...restProps }: Props = $props();
</script>
<svg
fill="currentColor"
aria-hidden="true"
viewBox="0 0 12 12"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
class={className}
{...restProps}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.3545 7.9775L4.7145 6.654L4.7545 6.539L4.7145 6.475H4.6L4.205 6.451L2.856 6.4145L1.6865 6.366L0.5535 6.305L0.268 6.2445L0 5.892L0.0275 5.716L0.2675 5.5555L0.6105 5.5855L1.3705 5.637L2.5095 5.716L3.3355 5.7645L4.56 5.892H4.7545L4.782 5.8135L4.715 5.7645L4.6635 5.716L3.4845 4.918L2.2085 4.074L1.5405 3.588L1.1785 3.3425L0.9965 3.1115L0.9175 2.6075L1.2455 2.2465L1.686 2.2765L1.7985 2.307L2.245 2.65L3.199 3.388L4.4445 4.3045L4.627 4.4565L4.6995 4.405L4.709 4.3685L4.627 4.2315L3.9495 3.0085L3.2265 1.7635L2.9045 1.2475L2.8195 0.938C2.78711 0.819128 2.76965 0.696687 2.7675 0.5735L3.1415 0.067L3.348 0L3.846 0.067L4.056 0.249L4.366 0.956L4.867 2.0705L5.6445 3.5855L5.8725 4.0345L5.994 4.4505L6.0395 4.578H6.1185V4.505L6.1825 3.652L6.301 2.6045L6.416 1.257L6.456 0.877L6.644 0.422L7.0175 0.176L7.3095 0.316L7.5495 0.6585L7.516 0.8805L7.373 1.806L7.0935 3.2575L6.9115 4.2285H7.0175L7.139 4.1075L7.6315 3.4545L8.4575 2.4225L8.8225 2.0125L9.2475 1.5605L9.521 1.345H10.0375L10.4175 1.9095L10.2475 2.4925L9.7155 3.166L9.275 3.737L8.643 4.587L8.248 5.267L8.2845 5.322L8.3785 5.312L9.8065 5.009L10.578 4.869L11.4985 4.7115L11.915 4.9055L11.9605 5.103L11.7965 5.5065L10.812 5.7495L9.6575 5.9805L7.938 6.387L7.917 6.402L7.9415 6.4325L8.716 6.5055L9.047 6.5235H9.858L11.368 6.636L11.763 6.897L12 7.216L11.9605 7.4585L11.353 7.7685L10.533 7.574L8.6185 7.119L7.9625 6.9545H7.8715V7.0095L8.418 7.5435L9.421 8.4485L10.6755 9.6135L10.739 9.9025L10.578 10.13L10.408 10.1055L9.3055 9.277L8.88 8.9035L7.917 8.0935H7.853V8.1785L8.075 8.503L9.2475 10.2635L9.3085 10.8035L9.2235 10.98L8.9195 11.0865L8.5855 11.0255L7.8985 10.063L7.191 8.9795L6.6195 8.008L6.5495 8.048L6.2125 11.675L6.0545 11.86L5.69 12L5.3865 11.7695L5.2255 11.396L5.3865 10.658L5.581 9.696L5.7385 8.931L5.8815 7.981L5.9665 7.665L5.9605 7.644L5.8905 7.653L5.1735 8.6365L4.0835 10.109L3.2205 11.0315L3.0135 11.1135L2.655 10.9285L2.6885 10.5975L2.889 10.303L4.083 8.785L4.803 7.844L5.268 7.301L5.265 7.222H5.2375L2.066 9.28L1.501 9.353L1.2575 9.125L1.288 8.752L1.4035 8.6305L2.3575 7.9745L2.3545 7.9775Z"
/>
</svg>
src/lib/registry/ai/open-in-chat/icons/cursor.svelte
<script lang="ts">
interface Props {
class?: string;
size?: string | number;
}
let { class: className = "", size = "1em", ...restProps }: Props = $props();
</script>
<svg aria-hidden="true"
version="1.1"
viewBox="0 0 466.73 532.09"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
class={className}
{...restProps}
>
<path
d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"
fill="currentColor"
/>
</svg>
src/lib/registry/ai/open-in-chat/icons/github.svelte
<script lang="ts">
interface Props {
class?: string;
size?: string | number;
}
let { class: className = "", size = "1em", ...restProps }: Props = $props();
</script>
<svg
fill="currentColor"
aria-hidden="true"
viewBox="0 0 24 24"
width={size}
height={size}
class={className}
{...restProps}
>
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/>
</svg>
src/lib/registry/ai/open-in-chat/icons/scira.svelte
<script lang="ts">
interface Props {
class?: string;
size?: string | number;
}
let { class: className = "", size = "1em", ...restProps }: Props = $props();
</script>
<svg aria-hidden="true"
fill="none"
height="934"
viewBox="0 0 910 934"
width="910"
class={className}
style:width={size}
style:height={size}
xmlns="http://www.w3.org/2000/svg"
{...restProps}
>
<path
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
fill="currentColor"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="8"
/>
<path
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
fill="currentColor"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="8"
/>
<path
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="20"
/>
<path
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="20"
/>
<path
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
fill="currentColor"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="8"
/>
<path
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
fill="currentColor"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="8"
/>
<path
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="30"
/>
</svg>
src/lib/registry/ai/open-in-chat/icons/t3.svelte
<script lang="ts">
import ChatCircleIcon from "phosphor-svelte/lib/ChatCircle";
import type { SVGAttributes } from "svelte/elements";
let { ...restProps }: SVGAttributes<SVGSVGElement> = $props();
</script>
<ChatCircleIcon
{...(restProps as object)}
/>
src/lib/registry/ai/open-in-chat/icons/v0.svelte
<script lang="ts">
interface Props {
class?: string;
size?: string | number;
}
let { class: className = "", size = "1em", ...restProps }: Props = $props();
</script>
<svg aria-hidden="true"
fill="currentColor"
viewBox="0 0 147 70"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
class={className}
{...restProps}
>
<path
d="M56 50.2031V14H70V60.1562C70 65.5928 65.5928 70 60.1562 70C57.5605 70 54.9982 68.9992 53.1562 67.1573L0 14H19.7969L56 50.2031Z"
/>
<path
d="M147 56H133V23.9531L100.953 56H133V70H96.6875C85.8144 70 77 61.1856 77 50.3125V14H91V46.1562L123.156 14H91V0H127.312C138.186 0 147 8.81439 147 19.6875V56Z"
/>
</svg>
src/lib/registry/ai/open-in-chat/index.ts
import Root from "./open-in.svelte";
import OpenInChatGPT from "./open-in-chatgpt.svelte";
import OpenInClaude from "./open-in-claude.svelte";
import Content from "./open-in-content.svelte";
import OpenInCursor from "./open-in-cursor.svelte";
import OpenInGitHub from "./open-in-github.svelte";
import Item from "./open-in-item.svelte";
import Label from "./open-in-label.svelte";
import OpenInScira from "./open-in-scira.svelte";
import Separator from "./open-in-separator.svelte";
import OpenInT3 from "./open-in-t3.svelte";
import Trigger from "./open-in-trigger.svelte";
import OpenInv0 from "./open-in-v0.svelte";
export {
type ProviderConfig,
type ProviderKey,
providers,
} from "./providers.js";
export {
Content,
Content as OpenInContent,
Item,
Item as OpenInItem,
Label,
Label as OpenInLabel,
OpenInChatGPT,
OpenInClaude,
OpenInCursor,
OpenInGitHub,
OpenInScira,
OpenInT3,
OpenInv0,
Root,
//
Root as OpenIn,
Separator,
Separator as OpenInSeparator,
Trigger,
Trigger as OpenInTrigger,
};
Part Prop Type Default
OpenIn query string
OpenInTrigger, OpenInContent, OpenInLabel, OpenInSeparator, OpenInItem menu parts
OpenInClaude, OpenInChatGPT, OpenInT3, OpenInScira, OpenInv0, OpenInCursor, OpenInGitHub provider items