feat: implement English Style Converter
- SvelteKit project scaffolded with TypeScript - Type definitions for Style, StyleCategory, ConversionRequest, ConversionResponse, LLMConfig - Style definitions with 6 categories and 25 sub-styles - Intensity mapping (1-5) with prompt modifier placeholders - LLM client using OpenAI-compatible API (Ollama default) - POST /api/convert endpoint with input validation - Animated loading modal with per-letter animations - Main page UI with category/style selectors, intensity slider - Copy to clipboard, collapsible prompt display - Vitest tests for styles, LLM prompt building, and API validation - Environment configuration for LLM settings
This commit is contained in:
256
src/lib/components/LoadingModal.svelte
Normal file
256
src/lib/components/LoadingModal.svelte
Normal file
@@ -0,0 +1,256 @@
|
||||
<script lang="ts">
|
||||
const loadingWords = [
|
||||
'Bamboozling',
|
||||
'Razzmatazzing',
|
||||
'Transmogrifying',
|
||||
'Alakazamming',
|
||||
'Prestidigitating',
|
||||
'Metamorphosizing',
|
||||
'Enchanting',
|
||||
'Voodooing',
|
||||
'Witchcrafting',
|
||||
'Sorcerizing',
|
||||
'Spellcasting',
|
||||
'Hocus-pocusing',
|
||||
'Incantating',
|
||||
'Conjurating',
|
||||
'Charmweaving'
|
||||
];
|
||||
|
||||
const colors = [
|
||||
'coral',
|
||||
'teal',
|
||||
'violet',
|
||||
'amber',
|
||||
'emerald',
|
||||
'rose',
|
||||
'skyblue',
|
||||
'fuchsia',
|
||||
'orange',
|
||||
'indigo'
|
||||
];
|
||||
|
||||
const colorValues: Record<string, string> = {
|
||||
coral: '#FF6B6B',
|
||||
teal: '#2EC4B6',
|
||||
violet: '#9B5DE5',
|
||||
amber: '#F5B041',
|
||||
emerald: '#2ECC71',
|
||||
rose: '#E74C6F',
|
||||
skyblue: '#5DADE2',
|
||||
fuchsia: '#D63384',
|
||||
orange: '#F39C12',
|
||||
indigo: '#5B2C6F'
|
||||
};
|
||||
|
||||
const animationStyles = [
|
||||
'slide-up',
|
||||
'bounce-in',
|
||||
'drop-in',
|
||||
'scale-up',
|
||||
'fade-rotate',
|
||||
'spring-left'
|
||||
];
|
||||
|
||||
let currentWordIndex = $state(0);
|
||||
let currentColor = $state(colors[0]);
|
||||
let currentAnimation = $state('slide-up');
|
||||
let letters = $state<string[]>([]);
|
||||
|
||||
$effect(() => {
|
||||
const interval = setInterval(() => {
|
||||
currentWordIndex = Math.floor(Math.random() * loadingWords.length);
|
||||
currentColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
currentAnimation = animationStyles[Math.floor(Math.random() * animationStyles.length)];
|
||||
letters = loadingWords[currentWordIndex].split('');
|
||||
}, 2000);
|
||||
|
||||
// Initialize immediately
|
||||
currentWordIndex = Math.floor(Math.random() * loadingWords.length);
|
||||
currentColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
currentAnimation = animationStyles[Math.floor(Math.random() * animationStyles.length)];
|
||||
letters = loadingWords[currentWordIndex].split('');
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="modal-overlay" role="dialog" aria-label="Loading">
|
||||
<div class="modal-content">
|
||||
<div class="loading-letters" style="color: {colorValues[currentColor]}">
|
||||
{#each letters as letter, i}
|
||||
<span
|
||||
class="letter {currentAnimation}"
|
||||
style="--delay: {i * 60}ms; --color: {colorValues[currentColor]}"
|
||||
>
|
||||
{letter}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="loading-subtitle">Transforming your text...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fade-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.loading-letters {
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
min-height: 4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.letter {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
animation-fill-mode: forwards;
|
||||
animation-duration: 0.4s;
|
||||
animation-delay: var(--delay);
|
||||
}
|
||||
|
||||
/* Slide up */
|
||||
.slide-up {
|
||||
animation-name: slide-up;
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Bounce in */
|
||||
.bounce-in {
|
||||
animation-name: bounce-in;
|
||||
}
|
||||
|
||||
@keyframes bounce-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Drop in */
|
||||
.drop-in {
|
||||
animation-name: drop-in;
|
||||
}
|
||||
|
||||
@keyframes drop-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-60px);
|
||||
}
|
||||
70% {
|
||||
opacity: 1;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
85% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Scale up */
|
||||
.scale-up {
|
||||
animation-name: scale-up;
|
||||
}
|
||||
|
||||
@keyframes scale-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fade rotate */
|
||||
.fade-rotate {
|
||||
animation-name: fade-rotate;
|
||||
}
|
||||
|
||||
@keyframes fade-rotate {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: rotate(-15deg) scale(0.5);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Spring from left */
|
||||
.spring-left {
|
||||
animation-name: spring-left;
|
||||
}
|
||||
|
||||
@keyframes spring-left {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-40px);
|
||||
}
|
||||
70% {
|
||||
opacity: 1;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-subtitle {
|
||||
margin-top: 1rem;
|
||||
color: #6b7280;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user