{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"grade-calculator","type":"registry:component","title":"Weighted Grade Calculator","description":"Estimate a weighted average from scores and course components.","author":"Notion Arena","categories":["docs"],"tags":["widget","free","grade calculator","weighted average","student","course","education"],"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/grade-calculator","fileUrl":"https://notionarena.vercel.app/widgets/grade-calculator.html","downloadUrl":"https://notionarena.vercel.app/api/widget/grade-calculator","height":360,"minHeight":260,"maxHeight":520,"params":[{"key":"label","label":"Widget title","type":"text","default":"Weighted grade"},{"key":"parts","label":"Starting components","type":"text","default":"Homework::90::25||Quizzes::85::25||Project::92::20","help":"Name::score percent::course weight percent, separated by ||."},{"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/grade-calculator.html","type":"registry:file","target":"notion/widgets/grade-calculator.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>Weighted Grade Calculator</title>\n<style>\n:root{--bg:#fff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#6940a5;--accent-ink:#fff;--accent-text:var(--accent)--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}\n@media(prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#343434}}\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:260px;display:flex;flex-direction:column;gap:8px;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}.average{font-size:17px;color:var(--accent-text);font-weight:700;font-variant-numeric:tabular-nums}.cols{display:grid;grid-template-columns:minmax(0,1fr) 60px 60px 24px;gap:4px;padding:0 3px;font-size:8px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}.rows{display:flex;min-height:72px;flex:1;flex-direction:column;gap:4px;overflow:auto}.row{display:grid;grid-template-columns:minmax(0,1fr) 60px 60px 24px;gap:4px;align-items:center}.row input{width:100%;min-width:0;height:28px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--fg);padding:4px 5px;font:inherit;font-size:10px}.row input[type=number]{text-align:right;font-variant-numeric:tabular-nums}.row input:focus-visible,.row button:focus-visible,.add:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.row button{width:24px;height:25px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--muted);cursor:pointer}.add{align-self:flex-start;min-height:27px;border:1px solid var(--border);border-radius:7px;background:var(--surface);color:var(--fg);font:inherit;font-size:9.5px;padding:4px 8px;cursor:pointer}.summary{display:flex;justify-content:space-between;gap:8px;font-size:9px;color:var(--muted)}.note{font-size:9px;color:var(--muted);line-height:1.35}\n@media(max-width:340px){.wrap{padding:10px}.cols,.row{grid-template-columns:minmax(0,1fr) 48px 48px 22px;gap:3px}.row input{padding:3px;font-size:9px}}\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\">Coursework helper</div><div class=\"title\" id=\"title\">Weighted grade</div></div><div class=\"average\" id=\"average\">0%</div></header>\n  <div class=\"cols\"><span>Component</span><span>Score %</span><span>Weight %</span><span></span></div>\n  <section class=\"rows\" id=\"rows\" aria-label=\"Graded course components\"></section>\n  <button class=\"add\" id=\"add\" type=\"button\">+ Add component</button>\n  <div class=\"summary\"><span id=\"weightTotal\">0% of course weight entered</span><span id=\"points\">0 weighted points</span></div>\n  <div class=\"note\">Average uses only the components and weights shown. Check your syllabus for the official grading rules. Nothing is saved or sent.</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  function rgb(hex){var s=String(hex).slice(1);return [0,2,4].map(function(i){return parseInt(s.slice(i,i+2),16);});}\n  function luminance(values){return values.map(function(v){v/=255;return v<=.04045?v/12.92:Math.pow((v+.055)/1.055,2.4);}).reduce(function(a,v,i){return a+v*[.2126,.7152,.0722][i];},0);}\n  function contrast(a,b){var x=luminance(a),y=luminance(b);return (Math.max(x,y)+.05)/(Math.min(x,y)+.05);}\n  function accentInk(hex){var c=rgb(hex);return contrast(c,[255,255,255])>=contrast(c,[0,0,0])?'#fff':'#000';}\n  function accentText(hex){var dark=theme==='dark'||(theme==='auto'&&window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches),bg=rgb(dark?'#252525':'#f7f7f5'),base=rgb(hex),target=dark?[255,255,255]:[0,0,0];if(contrast(base,bg)>=4.5)return hex;for(var t=.02;t<=1;t+=.02){var mix=base.map(function(v,i){return Math.round(v+(target[i]-v)*t);});if(contrast(mix,bg)>=4.5)return '#'+mix.map(function(v){return v.toString(16).padStart(2,'0');}).join('');}return dark?'#fff':'#191919';}\n  function setAccentText(){document.documentElement.style.setProperty('--accent-text',accentText(ACCENT));}\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||'Weighted grade').slice(0,48),parts=[];\n  String(c.parts||'Homework::90::25||Quizzes::85::25||Project::92::20').split('||').slice(0,12).forEach(function(row){var p=row.split('::'),name=(p[0]||'').trim(),score=parseFloat(p[1]),weight=parseFloat(p[2]);if(name)parts.push({name:name.slice(0,50),score:Math.max(0,Math.min(100,isFinite(score)?score:0)),weight:Math.max(0,Math.min(100,isFinite(weight)?weight:0))});});\n  document.documentElement.setAttribute('data-theme',theme);document.documentElement.style.setProperty('--accent',ACCENT);document.documentElement.style.setProperty('--accent-ink',accentInk(ACCENT));setAccentText();if(theme==='auto'&&window.matchMedia){var scheme=window.matchMedia('(prefers-color-scheme: dark)');if(scheme.addEventListener)scheme.addEventListener('change',setAccentText);else if(scheme.addListener)scheme.addListener(setAccentText);}document.getElementById('title').textContent=LABEL;document.title=LABEL;\n  var rows=document.getElementById('rows');\n  function calc(){var totalWeight=0,weighted=0;Array.from(rows.children).forEach(function(row){var score=Math.max(0,Math.min(100,parseFloat(row.querySelector('[data-score]').value)||0)),weight=Math.max(0,Math.min(100,parseFloat(row.querySelector('[data-weight]').value)||0));totalWeight+=weight;weighted+=score*weight;});var avg=totalWeight?weighted/totalWeight:0;document.getElementById('average').textContent=avg.toFixed(1)+'%';document.getElementById('weightTotal').textContent=totalWeight.toFixed(1)+'% of course weight entered';document.getElementById('points').textContent=(weighted/100).toFixed(1)+' weighted points';}\n  function addRow(item){var row=document.createElement('div'),name=document.createElement('input'),score=document.createElement('input'),weight=document.createElement('input'),remove=document.createElement('button');row.className='row';name.type='text';name.maxLength=50;name.value=item.name;name.setAttribute('aria-label','Component name');score.type='number';score.min='0';score.max='100';score.step='0.1';score.value=String(item.score);score.setAttribute('data-score','');score.setAttribute('aria-label','Score percentage');weight.type='number';weight.min='0';weight.max='100';weight.step='0.1';weight.value=String(item.weight);weight.setAttribute('data-weight','');weight.setAttribute('aria-label','Course weight percentage');remove.type='button';remove.textContent='×';remove.setAttribute('aria-label','Remove '+item.name);[name,score,weight].forEach(function(input){input.addEventListener('input',calc);});remove.addEventListener('click',function(){row.remove();calc();});row.appendChild(name);row.appendChild(score);row.appendChild(weight);row.appendChild(remove);rows.appendChild(row);}\n  parts.forEach(addRow);if(!parts.length)addRow({name:'Component',score:0,weight:0});\n  document.getElementById('add').addEventListener('click',function(){if(rows.children.length>=12)return;addRow({name:'New component',score:0,weight:0});calc();});calc();\n})();\n</script>\n</body>\n</html>\n"}]}