Add 6 dynamic components: rotary knob, dot loader, OTP, live counter, activity stream, copy field

This commit is contained in:
秉寒 2026-06-15 00:16:37 +08:00
parent 2eb46080b2
commit deefc282b4
4 changed files with 122 additions and 1 deletions

View File

@ -52,7 +52,7 @@ No package manager, no compiler — copy the files into any static site, or take
## Components
Foundations, forms, navigation, feedback, data display, and an applied dashboard — 40+ components, all in two themes.
Foundations, forms, navigation, feedback, data display, dynamic controls, and an applied app screen — 46 components, all in two themes.
![Foundations — color, dot-matrix type, iconography](docs/components.png)

View File

@ -459,4 +459,38 @@ td{font-family:var(--f-mono);font-size:11px;padding:9px 8px;border-bottom:1px so
@keyframes deployscan{0%{width:10%}85%{width:90%}100%{width:90%}}
.pill.sig{background:var(--accent);color:var(--accent-ink)}
@media(max-width:720px){.appscreen{flex-direction:column}.appscreen .rail{flex-direction:row;width:auto;border-right:0;border-bottom:1px solid var(--line)}.appscreen .rail .sp{display:none}}
/* ===== additional dynamic components (4146) — net-new types, Nothing-flavored ===== */
@keyframes llin{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
/* 41 rotary knob (drag) — Teenage-Engineering / hardware feel */
.knobwrap{text-align:center}
.knob{width:78px;height:78px;border-radius:50%;border:1px solid var(--line-2);background:var(--surface);position:relative;margin:0 auto;cursor:grab;touch-action:none}
.knob:active{cursor:grabbing}
.knob::after{content:"";position:absolute;inset:11px;border-radius:50%;border:1px solid var(--line)}
.knob .ind{position:absolute;left:50%;top:50%;width:2px;height:30px;background:var(--display);transform-origin:50% 100%;transform:translate(-50%,-100%) rotate(var(--a,-135deg))}
.knobval{font-family:var(--f-display);font-weight:500;font-size:22px;color:var(--display);margin-top:14px}
/* 42 dot-matrix spinner */
.dspin{position:relative;width:50px;height:50px;margin:14px auto}
.dspin i{position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--display);opacity:.15;animation:dfade 1s linear infinite}
@keyframes dfade{0%{opacity:1}70%,100%{opacity:.15}}
/* 43 verification (OTP) input */
.otp{display:flex;gap:8px}
.otp input{width:40px;height:50px;text-align:center;background:transparent;border:1px solid var(--line-2);border-radius:var(--r-sm);color:var(--display);font-family:var(--f-display);font-weight:500;font-size:22px;outline:0;transition:border-color .2s var(--ease)}
.otp input:focus{border-color:var(--primary)}
.otp.done input{border-color:var(--success);color:var(--success)}
/* 44 live counter (count-up) */
.ticker .tval{font-family:var(--f-display);font-weight:500;font-size:40px;color:var(--display);line-height:1}
.ticker .tdelta{font-family:var(--f-mono);font-size:11px;margin-top:8px;letter-spacing:.04em}
.ticker .tdelta.up{color:var(--success)} .ticker .tdelta.dn{color:var(--error)}
/* 45 activity stream */
.stream{display:flex;flex-direction:column;gap:7px;height:150px;overflow:hidden;font-family:var(--f-mono);font-size:10px}
.stream .s-l{display:flex;gap:8px;color:var(--secondary);animation:llin .3s var(--ease)}
.stream .s-l .t{color:var(--muted);flex-shrink:0}
.stream .s-l.sig{color:var(--accent-text)}
/* 46 copy field */
.copyfield{display:flex;align-items:center;gap:8px;border:1px solid var(--line-2);border-radius:var(--r-sm);padding:7px 7px 7px 12px}
.copyfield code{flex:1;min-width:0;font-family:var(--f-mono);font-size:11px;color:var(--primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.copyfield .cbtn{flex-shrink:0;font-family:var(--f-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;border:1px solid var(--line-2);border-radius:5px;padding:6px 10px;background:transparent;color:var(--secondary);cursor:pointer;transition:opacity .2s var(--ease)}
.copyfield .cbtn:hover{opacity:.7}
.copyfield .cbtn.ok{background:var(--display);color:var(--bg);border-color:var(--display)}
footer{padding:28px 24px;display:flex;justify-content:space-between;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.08em;border-top:1px solid var(--line)}

View File

@ -468,6 +468,46 @@
<div class="gmx-wrap" id="gmx"></div>
</div>
<!-- 41 ROTARY KNOB -->
<div class="mod s3">
<div class="mhead"><span class="no">41</span><span class="t">Rotary Knob</span><span class="ex">drag</span></div>
<div class="knobwrap"><div class="knob" id="knob"><span class="ind"></span></div><div class="knobval" id="knobval">50</div><div class="label" style="margin-top:4px">GAIN</div></div>
</div>
<!-- 42 DOT LOADER -->
<div class="mod s3">
<div class="mhead"><span class="no">42</span><span class="t">Dot Loader</span></div>
<div class="dspin" id="dspin"></div>
<div class="label" style="text-align:center;margin-top:6px">LOADING</div>
</div>
<!-- 43 VERIFICATION CODE -->
<div class="mod s3">
<div class="mhead"><span class="no">43</span><span class="t">Verification Code</span></div>
<div class="otp" id="otp"><input inputmode="numeric" maxlength="1"><input inputmode="numeric" maxlength="1"><input inputmode="numeric" maxlength="1"><input inputmode="numeric" maxlength="1"></div>
<div class="label" style="margin-top:10px">ENTER 4-DIGIT CODE</div>
</div>
<!-- 44 LIVE COUNTER -->
<div class="mod s3 ticker">
<div class="mhead"><span class="no">44</span><span class="t">Live Counter</span></div>
<div class="tval" data-to="1284">0</div>
<div class="tdelta up">↑ 12% THIS WEEK</div>
</div>
<!-- 45 ACTIVITY STREAM -->
<div class="mod s5">
<div class="mhead"><span class="no">45</span><span class="t">Activity Stream</span><span class="ex">live</span></div>
<div class="stream" id="stream"></div>
</div>
<!-- 46 COPY FIELD -->
<div class="mod s7">
<div class="mhead"><span class="no">46</span><span class="t">Copy Field</span></div>
<div class="copyfield"><code>git clone github.com/wangbh030722/vibe-nothing-ui-design</code><button class="cbtn" data-copy="git clone https://github.com/wangbh030722/vibe-nothing-ui-design">Copy</button></div>
<div class="label" style="margin-top:10px">CLICK TO COPY · CONFIRMS INLINE</div>
</div>
</div>
</div>

View File

@ -127,3 +127,50 @@ const t_dark=document.getElementById('t-dark'),t_light=document.getElementById('
/* Applied · Workspace screen: the deploying-row progress + live dot are pure CSS;
rail nav / tabs / pagination are handled by the delegated click handler above. */
/* ===== additional dynamic components (4146) ===== */
// 41 rotary knob — vertical drag, value 0100, indicator rotates -135°…135°
const knob=document.getElementById('knob'),knobval=document.getElementById('knobval');
if(knob&&knobval){let v=50,drag=false,sy=0,sv=50;
const set=x=>{v=Math.max(0,Math.min(100,x));knob.style.setProperty('--a',(-135+v/100*270)+'deg');knobval.textContent=Math.round(v);};
set(50);
knob.addEventListener('pointerdown',e=>{drag=true;sy=e.clientY;sv=v;knob.setPointerCapture(e.pointerId);});
knob.addEventListener('pointermove',e=>{if(drag)set(sv+(sy-e.clientY)*0.7);});
const stop=()=>drag=false;knob.addEventListener('pointerup',stop);knob.addEventListener('pointercancel',stop);}
// 42 dot-matrix spinner — 8 dots on a ring, staggered fade
const dspin=document.getElementById('dspin');
if(dspin){for(let i=0;i<8;i++){const d=document.createElement('i');
d.style.transform='rotate('+(i*45)+'deg) translateY(-19px)';d.style.animationDelay=(i/8)+'s';dspin.appendChild(d);}}
// 43 verification (OTP) input — auto-advance, backspace-back, success when full
const otp=document.getElementById('otp');
if(otp){const ins=[...otp.querySelectorAll('input')];
ins.forEach((inp,i)=>{
inp.addEventListener('input',()=>{inp.value=inp.value.replace(/\D/g,'').slice(0,1);
if(inp.value&&i<ins.length-1)ins[i+1].focus();
otp.classList.toggle('done',ins.every(x=>x.value));});
inp.addEventListener('keydown',e=>{if(e.key==='Backspace'&&!inp.value&&i>0)ins[i-1].focus();});
});}
// 44 live counter — count up with easing when scrolled into view
document.querySelectorAll('.ticker .tval[data-to]').forEach(el=>{const to=+el.getAttribute('data-to');
const io=new IntersectionObserver(es=>es.forEach(en=>{if(en.isIntersecting){let s=null;const D=1200;
const step=t=>{if(!s)s=t;const k=Math.min(1,(t-s)/D);const e=k<.5?4*k*k*k:1-Math.pow(-2*k+2,3)/2;
el.textContent=Math.round(to*e);if(k<1)requestAnimationFrame(step);else el.textContent=to;};
requestAnimationFrame(step);io.disconnect();}}),{threshold:.6});io.observe(el);});
// 45 activity stream — mono log lines appended live, newest on top
const stream=document.getElementById('stream');
if(stream){const L=[['user.auth · session opened',0],['build #482 · passed',0],['agent-core · 3 files changed',0],['rate limit · 12% used',0],['deploy · review required',1],['cache · purged',0],['webhook · delivered',0],['db · migration applied',0]];
let i=0;const pad=n=>String(n).padStart(2,'0');
const push=()=>{const d=new Date();const t=pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds());const[m,sig]=L[i++%L.length];
const row=document.createElement('div');row.className='s-l'+(sig?' sig':'');row.innerHTML='<span class="t">'+t+'</span>'+m;
stream.prepend(row);while(stream.children.length>7)stream.removeChild(stream.lastChild);};
for(let k=0;k<6;k++)push();setInterval(push,1900);}
// 46 copy field — click → "Copied" inline confirm
document.querySelectorAll('.cbtn[data-copy]').forEach(b=>b.addEventListener('click',()=>{
const t=b.getAttribute('data-copy');try{navigator.clipboard&&navigator.clipboard.writeText(t);}catch(_){}
const o=b.textContent;b.textContent='Copied';b.classList.add('ok');
setTimeout(()=>{b.textContent=o;b.classList.remove('ok');},1200);}));