{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"vocabulary-trainer","type":"registry:component","title":"Vocabulary Trainer","description":"Practice typed recall for a configurable vocabulary deck.","author":"Notion Arena","categories":["docs"],"tags":["widget","free","vocabulary","language","learning","study","active recall"],"meta":{"kind":"widget","tier":"free","version":"1.0.0","updatedAt":"2026-10-11","notionPlan":"Free","emoji":"🗣️","delivery":["embed","html-block","download"],"capabilities":{"interactive":true,"offlineSafe":true,"persistsLocally":false,"writesToNotion":false,"needsNetwork":false},"embedUrl":"https://notionarena.vercel.app/embed/vocabulary-trainer","fileUrl":"https://notionarena.vercel.app/widgets/vocabulary-trainer.html","downloadUrl":"https://notionarena.vercel.app/api/widget/vocabulary-trainer","height":300,"minHeight":235,"maxHeight":420,"params":[{"key":"label","label":"Deck title","type":"text","default":"Vocabulary trainer"},{"key":"pairs","label":"Words and meanings","type":"text","default":"","help":"Word::meaning, separated by ||. Up to 100 pairs."},{"key":"accent","label":"Accent","type":"color","default":"#6940a5"},{"key":"theme","label":"Theme","type":"select","default":"auto","options":[{"value":"auto","label":"Match Notion"},{"value":"light","label":"Light"},{"value":"dark","label":"Dark"}]}]},"files":[{"path":"widgets/vocabulary-trainer.html","type":"registry:file","target":"notion/widgets/vocabulary-trainer.html","content":"<!doctype html>\n<html lang=\"en\" data-theme=\"auto\">\n<head>\n<meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n<title>Vocabulary Trainer</title>\n<style>\n:root{--bg:#fff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#6940a5;--good:#0f7b6c;--bad:#c24136;--font:ui-sans-serif,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif}\nhtml[data-theme=\"dark\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#343434;--good:#4dab9a;--bad:#ff7369}\n@media(prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#343434;--good:#4dab9a;--bad:#ff7369}}\n*{box-sizing:border-box}html,body{margin:0;height:100%}body{background:var(--bg);color:var(--fg);font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;overflow:hidden}\n.wrap{height:100%;min-height:235px;display:flex;flex-direction:column;gap:9px;padding:13px;overflow:auto}.head{display:flex;align-items:center;justify-content:space-between;gap:8px}.kicker{font-size:9px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);font-weight:650}.title{font-size:13.5px;font-weight:650;margin-top:2px}.count{font-size:10px;color:var(--muted);font-variant-numeric:tabular-nums}\n.card{display:flex;flex:1;min-height:90px;flex-direction:column;justify-content:center;gap:8px;padding:13px 14px;border:1px solid var(--border);border-radius:10px;background:var(--surface)}.term{font-size:20px;font-weight:650;letter-spacing:-.03em;color:var(--accent);overflow-wrap:anywhere}.answer{font-size:11px;line-height:1.45;color:var(--fg)}.answer[hidden]{display:none}.field{display:flex;flex-direction:column;gap:4px}.field label{font-size:10px;color:var(--muted);font-weight:600}.field input{height:32px;border:1px solid var(--border);border-radius:7px;background:var(--surface);color:var(--fg);padding:5px 8px;font:inherit;font-size:11px}.field input:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.controls{display:flex;gap:6px;flex-wrap:wrap}.controls button{min-height:29px;border:1px solid var(--border);border-radius:7px;background:var(--surface);color:var(--fg);font:inherit;font-size:10.5px;padding:5px 8px;cursor:pointer}.controls button:hover{border-color:var(--accent)}.controls button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.controls .primary{background:var(--accent);border-color:var(--accent);color:#fff}.status{min-height:14px;font-size:10px;color:var(--muted)}.status.good{color:var(--good)}.status.bad{color:var(--bad)}\n@media(max-width:330px){.wrap{padding:10px}.term{font-size:18px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}\n</style>\n</head>\n<body><main class=\"wrap\">\n  <header class=\"head\"><div><div class=\"kicker\">Type, then check</div><div class=\"title\" id=\"title\">Vocabulary trainer</div></div><div class=\"count\" id=\"count\">1 of 4</div></header>\n  <section class=\"card\"><div class=\"term\" id=\"term\">Loading word</div><div class=\"answer\" id=\"answer\" hidden></div></section>\n  <form class=\"field\" id=\"form\"><label for=\"guess\">Your meaning</label><input id=\"guess\" autocomplete=\"off\" /></form>\n  <div class=\"controls\"><button class=\"primary\" id=\"check\" type=\"button\">Check</button><button id=\"reveal\" type=\"button\">Show meaning</button><button id=\"next\" type=\"button\">Next word</button></div>\n  <div class=\"status\" id=\"status\" aria-live=\"polite\">Your answers stay in this session.</div>\n</main>\n<script>\n(function(){\n  var cfgEl=document.getElementById('bk-config'),base={};try{base=cfgEl?JSON.parse(cfgEl.textContent||'{}'):{};}catch(e){}\n  var q=new URLSearchParams(location.search),c={};for(var k in base)c[k]=base[k];q.forEach(function(v,k){c[k]=v;});\n  function safeColor(v,d){return /^#[0-9a-f]{6}$/i.test(String(v||''))?String(v):d;}\n  var theme=['auto','light','dark'].indexOf(String(c.theme||'auto'))>-1?String(c.theme||'auto'):'auto',ACCENT=safeColor(c.accent,'#6940a5'),LABEL=String(c.label||'Vocabulary trainer').slice(0,48),pairs=[];\n  String(c.pairs||'').split('||').slice(0,100).forEach(function(row){var cut=row.indexOf('::');if(cut<0)return;var word=row.slice(0,cut).trim(),meaning=row.slice(cut+2).trim();if(word&&meaning)pairs.push({word:word.slice(0,100),meaning:meaning.slice(0,180)});});\n  if(!pairs.length)pairs=[{word:'सहज',meaning:'Natural, easy, or effortless.'},{word:'Poco a poco',meaning:'Little by little.'},{word:'Schadenfreude',meaning:'Pleasure derived from another person’s misfortune.'},{word:'Ubuntu',meaning:'A shared humanity or sense of community.'}];\n  document.documentElement.setAttribute('data-theme',theme);document.documentElement.style.setProperty('--accent',ACCENT);document.getElementById('title').textContent=LABEL;document.title=LABEL;\n  var index=0,score=0,checked=false,term=document.getElementById('term'),answer=document.getElementById('answer'),guess=document.getElementById('guess'),status=document.getElementById('status');\n  function render(){term.textContent=pairs[index].word;answer.textContent=pairs[index].meaning;answer.hidden=true;guess.value='';checked=false;document.getElementById('count').textContent=(index+1)+' of '+pairs.length+' · '+score+' correct';status.className='status';status.textContent='Type a meaning, then check it. Case and final . ? or ! are ignored.';}\n  document.getElementById('check').addEventListener('click',function(){var v=guess.value.trim().toLocaleLowerCase().replace(/[.!?]+$/g,''),target=pairs[index].meaning.trim().toLocaleLowerCase().replace(/[.!?]+$/g,'');if(!v){status.className='status bad';status.textContent='Type an answer first.';return;}if(checked){status.className='status';status.textContent='Already checked. Move to the next word to continue.';return;}checked=true;answer.hidden=false;if(v===target){score++;status.className='status good';status.textContent='Correct.';}else{status.className='status bad';status.textContent='Not an exact match. Compare with the meaning below.';}document.getElementById('count').textContent=(index+1)+' of '+pairs.length+' · '+score+' correct';});\n  document.getElementById('reveal').addEventListener('click',function(){answer.hidden=!answer.hidden;});\n  document.getElementById('next').addEventListener('click',function(){index=(index+1)%pairs.length;render();});\n  document.getElementById('form').addEventListener('submit',function(e){e.preventDefault();document.getElementById('check').click();});\n  render();\n})();\n</script>\n</body>\n</html>\n"}]}