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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 279 additions & 0 deletions src/lib/components/PromptBanner.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
<script lang="ts">
import { createCopy } from '$lib/utils/copy';
import { getPrompt, hasPrompt } from '$lib/utils/prompts';
import { fly } from 'svelte/transition';
import { Button, Icon } from '$lib/components/ui';
import { createDropdownMenu, melt } from '@melt-ui/svelte';
import { onMount } from 'svelte';

export let promptName: string;

const prompt = getPrompt(promptName) ?? '';
const exists = hasPrompt(promptName);
const { copied, copy } = createCopy(prompt);
onMount(() => {
copied.set(false);
});

type Ide = 'copy' | 'cursor' | 'chatgpt' | 'claude';

// options rendered directly in dropdown

// Local dropdown configured to open to the left (bottom-end)
const {
elements: { trigger, menu },
states: { open }
} = createDropdownMenu({
forceVisible: true,
positioning: {
placement: 'bottom-end'
}
});

function openIde(value: Ide) {
if (value === 'copy') {
copy();
return;
}

const text = encodeURIComponent(prompt);

// NOTE: Deep links are best-effort; fall back to copy if blocked
if (value === 'cursor') {
const url = `cursor://anysphere.cursor-deeplink/prompt?text=${text}`;
try {
window.location.href = url;
} catch {
copy();
}
return;
}

if (value === 'chatgpt') {
const url = `https://chatgpt.com/?prompt=${text}`;
window.open(url, '_blank', 'noopener,noreferrer');
return;
}

if (value === 'claude') {
const url = `https://claude.ai/new?q=${text}`;
window.open(url, '_blank', 'noopener,noreferrer');
return;
}
}
function handleMainClick() {
copy();
}
</script>

{#if exists}
<div class="ai-banner">
<div class="ai-banner__content">
<div class="ai-banner__title">
<Icon name="sparkle" class="text-primary" aria-hidden="true" />
<span>Use your AI agent</span>
</div>
<div class="ai-banner__actions">
<div class="flex">
<Button
variant="secondary"
onclick={handleMainClick}
aria-label={$copied ? 'Copied' : 'Copy prompt'}
class="no-right-radius"
>
<Icon name={$copied ? 'check' : 'copy'} aria-hidden="true" />
<span>{$copied ? 'Copied' : 'Copy prompt'}</span>
</Button>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Svelte events: use on:click, not onclick.

Current handlers won’t fire. Replace onclick with on:click.

Apply:

-                    <Button
+                    <Button
                         variant="secondary"
-                        onclick={handleMainClick}
+                        on:click={handleMainClick}
                         aria-label={$copied ? 'Copied' : 'Copy prompt'}
                         class="no-right-radius"
                     >
@@
-                                    <button
+                                    <button
                                         type="button"
                                         class="menu-btn"
-                                        onclick={() => {
+                                        on:click={() => {
                                             openIde('cursor');
                                         }}
                                     >
@@
-                                    <button
+                                    <button
                                         type="button"
                                         class="menu-btn"
-                                        onclick={() => {
+                                        on:click={() => {
                                             openIde('chatgpt');
                                         }}
                                     >
@@
-                                    <button
+                                    <button
                                         type="button"
                                         class="menu-btn"
-                                        onclick={() => {
+                                        on:click={() => {
                                             openIde('claude');
                                         }}
                                     >

Also applies to: 103-110, 129-136, 154-160

🤖 Prompt for AI Agents
In src/lib/components/PromptBanner.svelte around lines 78-87 (and also at
103-110, 129-136, 154-160), the component uses the HTML attribute onclick which
does not wire Svelte event handlers; replace each onclick={...} with Svelte's
on:click={...} so the handlers fire correctly, preserving the same handler
names, props, and aria attributes.

<button
class="no-left-radius web-button is-secondary"
use:melt={$trigger}
aria-label="Open options"
>
<span class="web-icon-chevron-down" aria-hidden="true"></span>
</button>

{#if $open}
<div
class="menu-wrapper web-select-menu is-normal menu z-1"
use:melt={$menu}
transition:fly={{ y: 8, duration: 250 }}
>
<ul class="text-sub-body">
<li>
<button
type="button"
class="menu-btn"
onclick={() => {
openIde('cursor');
}}
>
<img
src="/images/docs/mcp/logos/dark/cursor-ai.svg"
alt=""
class="web-u-only-dark"
width="16"
height="16"
/>
<img
src="/images/docs/mcp/logos/cursor-ai.svg"
alt=""
class="web-u-only-light"
width="16"
height="16"
/>
<span>Open in Cursor</span>
</button>
</li>
<li>
<button
type="button"
class="menu-btn"
onclick={() => {
openIde('chatgpt');
}}
>
<img
src="/images/docs/mcp/logos/dark/openai.svg"
alt=""
class="web-u-only-dark"
width="16"
height="16"
/>
<img
src="/images/docs/mcp/logos/openai.svg"
alt=""
class="web-u-only-light"
width="16"
height="16"
/>
<span>Ask ChatGPT</span>
</button>
</li>
<li>
<button
type="button"
class="menu-btn"
onclick={() => {
openIde('claude');
}}
>
<img
src="/images/docs/mcp/logos/dark/claude.svg"
alt=""
class="web-u-only-dark"
width="16"
height="16"
/>
<img
src="/images/docs/mcp/logos/claude.svg"
alt=""
class="web-u-only-light"
width="16"
height="16"
/>
<span>Ask Claude</span>
</button>
</li>
</ul>
</div>
{/if}
</div>
</div>
</div>
</div>
{/if}

<style lang="scss">
.ai-banner {
padding: 12px 16px;
border: 1px solid hsl(var(--web-color-border));
border-radius: 12px;
background: hsl(var(--web-color-surface));
margin-block: 12px 16px;

&__content {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do use 2 underscores for classes?

display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}

&__title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}

&__actions {
display: flex;
align-items: center;
gap: 8px;
}
}
/* dropdown styles */
.menu-wrapper {
--p-card-border-radius: 0.5rem;
padding: 4px;
backdrop-filter: blur(2px);
--webkit-backdrop-filter: blur(2px);
z-index: 100;
}

ul {
min-width: 14.375rem;
display: flex;
flex-direction: column;
gap: 2px;
}

.menu-btn {
height: 32px;
display: flex;
align-items: center;
gap: 0.5rem;
border-radius: 0.5rem;
padding: 5px 10px;
width: 100%;
text-align: left;
}

.menu-btn:hover {
cursor: pointer;
background-color: hsl(var(--web-color-offset));
}

/* Force caret icon to be white inside secondary button */
.ai-banner [class*='icon'] {
color: hsl(var(--web-color-white));
}

:global(.ai-banner .web-button.no-left-radius [class*='icon']) {
color: hsl(var(--web-color-white));
}

/* Style child component output: adjust both element and its gradient pseudo-elements */
.ai-banner :global(.web-button.no-left-radius),
.ai-banner :global(.web-button.no-left-radius)::before,
.ai-banner :global(.web-button.no-left-radius)::after {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 0;
}

.ai-banner :global(.web-button.no-right-radius),
.ai-banner :global(.web-button.no-right-radius)::before,
.ai-banner :global(.web-button.no-right-radius)::after {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}

.ai-banner :global(.web-button),
.ai-banner :global(.web-button)::before,
.ai-banner :global(.web-button)::after {
padding-left: 10px;
padding-right: 10px;
}
</style>
41 changes: 41 additions & 0 deletions src/lib/utils/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Utility to load prompt text files from `src/prompts`, including nested folders
// Consumers must pass the file path relative to `src/prompts/`, including extension

const promptsGlob = import.meta.glob('/src/prompts/**/*', {
query: '?raw',
import: 'default',
eager: true
}) as Record<string, string>;

function toRelativeKey(fullPath: string): string {
// Convert absolute like "/src/prompts/foo/bar.md" to "foo/bar.md"
return fullPath.replace(/^\/?src\/prompts\//, '');
}

function normalizeInputKey(input: string): string {
// Normalize user input like "./foo\\bar.md" → "foo/bar.md"
return input.replace(/^\/*/, '').replace(/\\/g, '/');
}

const nameToPrompt: Record<string, string> = Object.entries(promptsGlob).reduce(
(acc, [path, contents]) => {
const key = toRelativeKey(path);
acc[key] = contents;
return acc;
},
{} as Record<string, string>
);

export function getPrompt(filePathWithExt: string): string | null {
const key = normalizeInputKey(filePathWithExt);
return nameToPrompt[key] ?? null;
}

export function hasPrompt(filePathWithExt: string): boolean {
const key = normalizeInputKey(filePathWithExt);
return key in nameToPrompt;
}

export function listPrompts(): string[] {
return Object.keys(nameToPrompt).sort();
}
5 changes: 5 additions & 0 deletions src/markdoc/layouts/Article.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { MainFooter } from '$lib/components';
import SeoOgImage from '$lib/components/SeoOgImage.svelte';
import { DocsArticle } from '$lib/layouts';
import PromptBanner from '$lib/components/PromptBanner.svelte';
import type { TocItem } from '$lib/layouts/DocsArticle.svelte';
import { DOCS_TITLE_SUFFIX, OVERVIEW_TITLE_SUFFIX } from '$routes/titles';
import { getContext, setContext } from 'svelte';
Expand All @@ -29,6 +30,7 @@
export let difficulty: string | undefined = undefined;
export let readtime: string | undefined = undefined;
export let date: string | undefined = undefined;
export let prompt: string | undefined = undefined;
setContext<LayoutContext>('headings', writable({}));
Expand Down Expand Up @@ -87,6 +89,9 @@
<li>{readtime} min</li>
{/if}
</svelte:fragment>
{#if prompt}
<PromptBanner promptName={prompt} />
{/if}
<slot />
</DocsArticle>
<MainFooter variant="docs" />
Loading
Loading