feat(global): implement main phase
This commit is contained in:
351
src/lib/components/PromptBuilder/PromptCard.svelte
Normal file
351
src/lib/components/PromptBuilder/PromptCard.svelte
Normal file
@@ -0,0 +1,351 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user