Files
agent-ui/user-components/test-counter/TestCounter.vue
josedario87 d27da30494 feat: Replace DB component tools with filesystem-based user-components/
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.
2026-02-18 10:24:05 -06:00

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>