Components are now .vue files in user-components/<folder>/ parsed at runtime. Replaces 6 DB MCP tools with 2 (list_fs_components, load_fs_component). Adds vue-parser, fs-components API, and file watcher for live reload.
248 lines
7.5 KiB
Vue
248 lines
7.5 KiB
Vue
<template>
|
|
<div class="counter" ref="counterEl">
|
|
<div class="fx-layer" ref="fxLayer"></div>
|
|
<h2 class="title">{{ title }}</h2>
|
|
<p class="count-display" :class="pulseClass" ref="countEl">{{ count }}</p>
|
|
<div class="buttons">
|
|
<button class="btn btn-minus" @click="decrement">-1</button>
|
|
<button class="btn btn-plus" @click="increment">+1</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const count = ref(0)
|
|
const title = 'Test Counter'
|
|
const pulseClass = ref('')
|
|
const countEl = ref(null)
|
|
const counterEl = ref(null)
|
|
const fxLayer = ref(null)
|
|
|
|
const fxStyles = [
|
|
{ pos: 'sparkle', neg: 'ember' },
|
|
{ pos: 'bubble', neg: 'drip' },
|
|
{ pos: 'star', neg: 'crack' },
|
|
{ pos: 'ring', neg: 'smoke' }
|
|
]
|
|
let styleIdx = 0
|
|
|
|
// Inject stylesheet once
|
|
if (!document.getElementById('counter-fx-css')) {
|
|
const s = document.createElement('style')
|
|
s.id = 'counter-fx-css'
|
|
s.textContent = `
|
|
.ctr-fx {
|
|
position: absolute;
|
|
width: var(--sz);
|
|
height: var(--sz);
|
|
border-radius: 50%;
|
|
pointer-events: none;
|
|
animation-delay: var(--del);
|
|
animation-duration: var(--dur);
|
|
animation-fill-mode: forwards;
|
|
animation-timing-function: cubic-bezier(.25,.46,.45,.94);
|
|
}
|
|
.ctr-fx-sparkle {
|
|
background: radial-gradient(circle, #fde047, #f59e0b);
|
|
box-shadow: 0 0 8px #fbbf24;
|
|
border-radius: 2px;
|
|
transform: rotate(45deg);
|
|
animation-name: ctr-sparkle;
|
|
}
|
|
@keyframes ctr-sparkle {
|
|
0% { opacity:1; transform: translate(0,0) rotate(45deg) scale(1); }
|
|
100% { opacity:0; transform: translate(var(--dx),var(--dy)) rotate(200deg) scale(0); }
|
|
}
|
|
.ctr-fx-ember {
|
|
background: radial-gradient(circle, #fca5a5, #dc2626);
|
|
box-shadow: 0 0 10px #ef4444;
|
|
animation-name: ctr-ember;
|
|
}
|
|
@keyframes ctr-ember {
|
|
0% { opacity:1; transform: translate(0,0) scale(1); }
|
|
100% { opacity:0; transform: translate(var(--dx), calc(var(--dy) + 80px)) scale(0.2); }
|
|
}
|
|
.ctr-fx-bubble {
|
|
background: radial-gradient(circle at 30% 30%, rgba(16,185,129,0.7), rgba(5,150,105,0.2));
|
|
border: 1.5px solid rgba(16,185,129,0.5);
|
|
animation-name: ctr-bubble;
|
|
}
|
|
@keyframes ctr-bubble {
|
|
0% { opacity:0.9; transform: translate(0,0) scale(0.5); }
|
|
50% { opacity:0.7; transform: translate(calc(var(--dx)*0.5), calc(var(--dy)*0.5)) scale(1.3); }
|
|
100% { opacity:0; transform: translate(var(--dx), calc(var(--dy) - 40px)) scale(1.6); }
|
|
}
|
|
.ctr-fx-drip {
|
|
background: radial-gradient(circle, #c084fc, #7c3aed);
|
|
border-radius: 50% 50% 50% 0;
|
|
animation-name: ctr-drip;
|
|
}
|
|
@keyframes ctr-drip {
|
|
0% { opacity:1; transform: translate(0,0) rotate(45deg) scale(1); }
|
|
100% { opacity:0; transform: translate(var(--dx), var(--dy)) rotate(45deg) scaleY(1.8) scale(0.3); }
|
|
}
|
|
.ctr-fx-star {
|
|
background: white;
|
|
box-shadow: 0 0 10px #22d3ee, 0 0 20px rgba(34,211,238,0.4);
|
|
clip-path: polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%);
|
|
border-radius: 0;
|
|
animation-name: ctr-star;
|
|
}
|
|
@keyframes ctr-star {
|
|
0% { opacity:1; transform: translate(0,0) scale(0.3) rotate(0); }
|
|
50% { opacity:1; transform: translate(calc(var(--dx)*0.6),calc(var(--dy)*0.6)) scale(1.3) rotate(90deg); }
|
|
100% { opacity:0; transform: translate(var(--dx),var(--dy)) scale(0) rotate(180deg); }
|
|
}
|
|
.ctr-fx-crack {
|
|
background: linear-gradient(135deg, #f97316, #dc2626);
|
|
clip-path: polygon(20% 0%,80% 0%,100% 60%,60% 100%,0% 80%);
|
|
border-radius: 0;
|
|
animation-name: ctr-crack;
|
|
}
|
|
@keyframes ctr-crack {
|
|
0% { opacity:1; transform: translate(0,0) scale(1) rotate(0); }
|
|
100% { opacity:0; transform: translate(var(--dx),var(--dy)) scale(0.2) rotate(270deg); }
|
|
}
|
|
.ctr-fx-ring {
|
|
background: transparent;
|
|
border: 2.5px solid #34d399;
|
|
box-shadow: 0 0 8px rgba(52,211,153,0.6);
|
|
animation-name: ctr-ring;
|
|
}
|
|
@keyframes ctr-ring {
|
|
0% { opacity:1; transform: translate(0,0) scale(0.3); }
|
|
100% { opacity:0; transform: translate(var(--dx),var(--dy)) scale(2.5); }
|
|
}
|
|
.ctr-fx-smoke {
|
|
background: radial-gradient(circle, rgba(120,113,108,0.6), rgba(68,64,60,0.1));
|
|
filter: blur(3px);
|
|
animation-name: ctr-smoke;
|
|
}
|
|
@keyframes ctr-smoke {
|
|
0% { opacity:0.8; transform: translate(0,0) scale(0.8); }
|
|
100% { opacity:0; transform: translate(var(--dx), calc(var(--dy) - 50px)) scale(3); }
|
|
}
|
|
`
|
|
document.head.appendChild(s)
|
|
}
|
|
|
|
function spawnParticles(type) {
|
|
const layer = fxLayer.value
|
|
const numEl = countEl.value
|
|
if (!layer || !numEl) return
|
|
|
|
const layerRect = layer.getBoundingClientRect()
|
|
const numRect = numEl.getBoundingClientRect()
|
|
const cx = numRect.left + numRect.width / 2 - layerRect.left
|
|
const cy = numRect.top + numRect.height / 2 - layerRect.top
|
|
|
|
const n = 14 + Math.floor(Math.random() * 8)
|
|
for (let i = 0; i < n; i++) {
|
|
const el = document.createElement('div')
|
|
el.className = 'ctr-fx ctr-fx-' + type
|
|
const angle = (Math.PI * 2 * i) / n + (Math.random() - 0.5) * 0.7
|
|
const dist = 60 + Math.random() * 120
|
|
const dx = Math.cos(angle) * dist
|
|
const rawDy = Math.sin(angle) * dist
|
|
const dy = type === 'drip' ? Math.abs(rawDy) + 50 : rawDy
|
|
const size = 6 + Math.random() * 16
|
|
el.style.cssText = [
|
|
'left:' + cx + 'px',
|
|
'top:' + cy + 'px',
|
|
'--dx:' + dx + 'px',
|
|
'--dy:' + dy + 'px',
|
|
'--sz:' + size + 'px',
|
|
'--del:' + (i * 20) + 'ms',
|
|
'--dur:' + (500 + Math.random() * 500) + 'ms'
|
|
].join(';')
|
|
layer.appendChild(el)
|
|
setTimeout(() => el.remove(), 1300)
|
|
}
|
|
}
|
|
|
|
function triggerFx(isPositive) {
|
|
const pair = fxStyles[styleIdx % fxStyles.length]
|
|
spawnParticles(isPositive ? pair.pos : pair.neg)
|
|
styleIdx++
|
|
pulseClass.value = isPositive ? 'pulse-up' : 'pulse-down'
|
|
setTimeout(() => { pulseClass.value = '' }, 350)
|
|
}
|
|
|
|
function increment() {
|
|
count.value++
|
|
triggerFx(true)
|
|
}
|
|
|
|
function decrement() {
|
|
count.value--
|
|
triggerFx(false)
|
|
}
|
|
|
|
return { count, title, pulseClass, countEl, counterEl, fxLayer, increment, decrement }
|
|
</script>
|
|
|
|
<style>
|
|
.counter {
|
|
position: relative;
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
font-family: system-ui, sans-serif;
|
|
background: transparent;
|
|
width: 100%;
|
|
height: 100%;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.fx-layer {
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
overflow: visible;
|
|
}
|
|
.title {
|
|
position: relative;
|
|
z-index: 1;
|
|
font-size: 1rem;
|
|
opacity: 0.7;
|
|
margin: 0 0 0.25rem;
|
|
font-weight: 400;
|
|
}
|
|
.count-display {
|
|
position: relative;
|
|
z-index: 1;
|
|
font-size: 3.5rem;
|
|
font-weight: 800;
|
|
margin: 0.25rem 0;
|
|
transition: transform 0.2s cubic-bezier(.34,1.56,.64,1), color 0.3s;
|
|
}
|
|
.pulse-up { transform: scale(1.15); color: #10b981; }
|
|
.pulse-down { transform: scale(0.88); color: #ef4444; }
|
|
.buttons {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
justify-content: center;
|
|
margin-top: 1rem;
|
|
}
|
|
.btn {
|
|
padding: 0.6rem 1.4rem;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
border: none;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
}
|
|
.btn:active { transform: scale(0.9); }
|
|
.btn:hover { transform: translateY(-2px); box-shadow: 0 4px 14px rgba(0,0,0,0.25); }
|
|
.btn-plus { background: #10b981; color: white; }
|
|
.btn-plus:hover { background: #059669; }
|
|
.btn-minus { background: #ef4444; color: white; }
|
|
.btn-minus:hover { background: #dc2626; }
|
|
</style>
|