352 lines
8.3 KiB
Svelte
352 lines
8.3 KiB
Svelte
<script lang="ts">
|
|
import { getI18n, type PromptPreset } from '$lib/i18n';
|
|
import { generateRaycastUrl, getPrompt } from '$lib/utils/raycast';
|
|
|
|
interface Props {
|
|
preset: PromptPreset;
|
|
}
|
|
|
|
let { preset }: Props = $props();
|
|
|
|
const i18n = getI18n();
|
|
const t = $derived(i18n.t);
|
|
|
|
let copied = $state(false);
|
|
|
|
const promptText = $derived(getPrompt(i18n.lang === 'ru' ? 'Russian' : 'English'));
|
|
|
|
const raycastUrl = $derived(
|
|
preset.id !== 'copy'
|
|
? generateRaycastUrl({
|
|
name: preset.name,
|
|
instructions: promptText,
|
|
model: preset.model,
|
|
reasoning_effort: preset.reasoning_effort,
|
|
creativity: 'none',
|
|
web_search: true,
|
|
})
|
|
: ''
|
|
);
|
|
|
|
async function copyPrompt() {
|
|
try {
|
|
await navigator.clipboard.writeText(promptText);
|
|
copied = true;
|
|
setTimeout(() => {
|
|
copied = false;
|
|
}, 2000);
|
|
} catch (err) {
|
|
console.error('Failed to copy:', err);
|
|
}
|
|
}
|
|
|
|
function getIcon(iconName: string): string {
|
|
switch (iconName) {
|
|
case 'beaver':
|
|
return '匠'; // master craftsman / professional
|
|
case 'zap':
|
|
return '閃'; // flash / lightning
|
|
case 'clipboard':
|
|
return '写'; // copy / transcribe
|
|
default:
|
|
return '道';
|
|
}
|
|
}
|
|
|
|
const cardVariant = $derived(
|
|
preset.id === 'pro' ? 'pro' : preset.id === 'flash' ? 'flash' : 'copy'
|
|
);
|
|
|
|
let svgElement: SVGSVGElement | null = $state(null);
|
|
let isHovering = $state(false);
|
|
let isTransitioningOut = $state(false);
|
|
|
|
function handleMouseEnter() {
|
|
isHovering = true;
|
|
isTransitioningOut = false;
|
|
}
|
|
|
|
function handleMouseLeave() {
|
|
if (!svgElement) {
|
|
isHovering = false;
|
|
return;
|
|
}
|
|
|
|
// Grab current animated values
|
|
const computed = getComputedStyle(svgElement);
|
|
const currentTransform = computed.transform;
|
|
const currentStrokeWidth = computed.strokeWidth;
|
|
|
|
// Apply them as inline styles
|
|
svgElement.style.transform = currentTransform === 'none' ? '' : currentTransform;
|
|
svgElement.style.strokeWidth = currentStrokeWidth;
|
|
|
|
// Stop animation, start transition
|
|
isHovering = false;
|
|
isTransitioningOut = true;
|
|
|
|
// Remove inline styles after a frame to let transition kick in
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
if (svgElement) {
|
|
svgElement.style.transform = '';
|
|
svgElement.style.strokeWidth = '';
|
|
}
|
|
// Reset after transition completes
|
|
setTimeout(() => {
|
|
isTransitioningOut = false;
|
|
}, 1000);
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
|
|
{#snippet cardContent()}
|
|
<header class="card-header">
|
|
<div class="card-icon">
|
|
<span class="icon-text">{getIcon(preset.icon)}</span>
|
|
</div>
|
|
<h3 class="card-title">{preset.name}</h3>
|
|
<span class="action-button" aria-hidden="true">
|
|
{#if preset.id === 'copy'}
|
|
{#if copied}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>
|
|
{:else}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
</svg>
|
|
{/if}
|
|
{:else}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<path d="M9 6l6 6-6 6"/>
|
|
</svg>
|
|
{/if}
|
|
</span>
|
|
</header>
|
|
<div class="divider"></div>
|
|
<p class="card-description">{preset.description}</p>
|
|
|
|
<!-- Decorative illustration -->
|
|
<div class="card-illustration" class:is-hovering={isHovering}>
|
|
{#if cardVariant === 'pro'}
|
|
<!-- Brain icon from Tabler Icons -->
|
|
<svg bind:this={svgElement} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="illustration-svg" class:breathing={isHovering}>
|
|
<path d="M15.5 13a3.5 3.5 0 0 0 -3.5 3.5v1a3.5 3.5 0 0 0 7 0v-1.8"/>
|
|
<path d="M8.5 13a3.5 3.5 0 0 1 3.5 3.5v1a3.5 3.5 0 0 1 -7 0v-1.8"/>
|
|
<path d="M17.5 16a3.5 3.5 0 0 0 0 -7h-.5"/>
|
|
<path d="M19 9.3v-2.8a3.5 3.5 0 0 0 -7 0"/>
|
|
<path d="M6.5 16a3.5 3.5 0 0 1 0 -7h.5"/>
|
|
<path d="M5 9.3v-2.8a3.5 3.5 0 0 1 7 0v10"/>
|
|
</svg>
|
|
{:else if cardVariant === 'flash'}
|
|
<!-- Lightning bolt -->
|
|
<svg bind:this={svgElement} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="illustration-svg" class:breathing={isHovering}>
|
|
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
|
</svg>
|
|
{:else}
|
|
<!-- @ symbol -->
|
|
<svg bind:this={svgElement} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="illustration-svg" class:breathing={isHovering}>
|
|
<circle cx="12" cy="12" r="4"/>
|
|
<path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/>
|
|
</svg>
|
|
{/if}
|
|
</div>
|
|
{/snippet}
|
|
|
|
{#if preset.id === 'copy'}
|
|
<div
|
|
class="prompt-card variant-{cardVariant}"
|
|
role="button"
|
|
tabindex="0"
|
|
onclick={copyPrompt}
|
|
onkeydown={(e) => e.key === 'Enter' && copyPrompt()}
|
|
onmouseenter={handleMouseEnter}
|
|
onmouseleave={handleMouseLeave}
|
|
>
|
|
{@render cardContent()}
|
|
</div>
|
|
{:else}
|
|
<a
|
|
href={raycastUrl}
|
|
class="prompt-card variant-{cardVariant}"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onmouseenter={handleMouseEnter}
|
|
onmouseleave={handleMouseLeave}
|
|
>
|
|
{@render cardContent()}
|
|
</a>
|
|
{/if}
|
|
|
|
<style>
|
|
.prompt-card {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 1.5rem;
|
|
gap: 1rem;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
min-height: 420px;
|
|
border-radius: 16px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Darkening overlay */
|
|
.prompt-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.25);
|
|
opacity: 0;
|
|
transition: opacity 1s ease;
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.prompt-card:hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Card variants with unique gradients */
|
|
.variant-pro {
|
|
background: linear-gradient(145deg, #191B43 0%, #0E0A2A 100%);
|
|
border: 1px solid rgba(25, 27, 67, 0.5);
|
|
}
|
|
|
|
.variant-flash {
|
|
background: linear-gradient(145deg, #122241 0%, #1E417A 100%);
|
|
border: 1px solid rgba(30, 65, 122, 0.5);
|
|
}
|
|
|
|
.variant-copy {
|
|
background: linear-gradient(145deg, #321134 0%, #260C28 100%);
|
|
border: 1px solid rgba(50, 17, 52, 0.5);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.card-icon {
|
|
width: 44px;
|
|
height: 44px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
backdrop-filter: blur(8px);
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.icon-text {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
color: white;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: rgba(255, 255, 255, 0.9);
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.action-button {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
flex-shrink: 0;
|
|
cursor: pointer;
|
|
transition: background-color 1s ease, color 1s ease;
|
|
}
|
|
|
|
.prompt-card:hover .action-button {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
color: #fff;
|
|
}
|
|
|
|
.divider {
|
|
height: 1px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.card-description {
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
color: rgba(255, 255, 255, 0.9);
|
|
line-height: 1.7;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* Decorative illustration - 50% of card, overflows edge */
|
|
.card-illustration {
|
|
position: absolute;
|
|
bottom: -25%;
|
|
right: -10%;
|
|
width: 85%;
|
|
height: 85%;
|
|
pointer-events: none;
|
|
opacity: 0.1;
|
|
transition: opacity 1s ease;
|
|
}
|
|
|
|
.card-illustration.is-hovering {
|
|
opacity: 0.18;
|
|
}
|
|
|
|
.illustration-svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
stroke-width: 1.2;
|
|
transform: scale(1) translateY(0);
|
|
transition: transform 1s ease, stroke-width 1s ease;
|
|
}
|
|
|
|
.illustration-svg.breathing {
|
|
animation: breathe 3s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes breathe {
|
|
0%, 100% {
|
|
transform: scale(1) translateY(0);
|
|
stroke-width: 1.2;
|
|
}
|
|
50% {
|
|
transform: scale(1.05) translateY(-2%);
|
|
stroke-width: 1.8;
|
|
}
|
|
}
|
|
|
|
.variant-pro .illustration-svg {
|
|
stroke: rgba(147, 130, 255, 0.9);
|
|
}
|
|
|
|
.variant-flash .illustration-svg {
|
|
stroke: rgba(100, 180, 255, 0.9);
|
|
}
|
|
|
|
.variant-copy .illustration-svg {
|
|
stroke: rgba(255, 130, 200, 0.9);
|
|
}
|
|
</style>
|