{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"kpi-strip","type":"registry:component","title":"KPI Strip","description":"Four metric tiles with sparklines, deltas and target rings.","author":"Notion Arena","categories":["dashboards"],"tags":["widget","free","kpi","metrics","sparkline","dashboard","chart"],"meta":{"kind":"widget","tier":"free","version":"2.0.0","updatedAt":"2026-10-06","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/kpi-strip","fileUrl":"https://notionarena.vercel.app/widgets/kpi-strip.html","downloadUrl":"https://notionarena.vercel.app/api/widget/kpi-strip","height":180,"minHeight":150,"maxHeight":300,"params":[{"key":"data","label":"Metrics (JSON)","type":"text","default":"","help":"Array of {label, value, unit, delta, target, series[]}. Leave blank for the demo dataset."},{"key":"accent","label":"Accent","type":"color","default":"#0f7b6c"},{"key":"theme","label":"Theme","type":"select","default":"auto","options":[{"value":"auto","label":"Match Notion"},{"value":"light","label":"Light"},{"value":"dark","label":"Dark"}]},{"key":"cols","label":"Columns","type":"select","default":"4","options":[{"value":"2","label":"2"},{"value":"3","label":"3"},{"value":"4","label":"4"}]}]},"files":[{"path":"widgets/kpi-strip.html","type":"registry:file","target":"notion/widgets/kpi-strip.html","content":"<!doctype html>\n<html lang=\"en\" data-theme=\"auto\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n<title>KPI Strip</title>\n<style>\n:root{--bg:#ffffff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#0f7b6c;\n--good:#0f7b6c;--bad:#e03e3e;--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:#2f2f2f;--good:#4dab9a;--bad:#ff7369}\n@media (prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#2f2f2f;--good:#4dab9a;--bad:#ff7369}}\n*{box-sizing:border-box}html,body{margin:0;height:100%}\nbody{background:var(--bg);color:var(--fg);font-family:var(--font);font-size:13px;-webkit-font-smoothing:antialiased;overflow:hidden}\n.wrap{height:100%;padding:12px;display:grid;gap:10px;grid-template-columns:repeat(var(--cols,4),minmax(0,1fr));align-content:center}\n.tile{border:1px solid var(--border);border-radius:10px;background:var(--surface);padding:11px 12px;display:flex;flex-direction:column;gap:7px;min-width:0;position:relative;overflow:hidden}\n.head{display:flex;align-items:baseline;justify-content:space-between;gap:6px}\n.name{font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.value{font-size:24px;font-weight:640;letter-spacing:-.035em;line-height:1.1;font-variant-numeric:tabular-nums;white-space:nowrap}\n.value small{font-size:13px;font-weight:500;color:var(--muted);letter-spacing:-.01em;margin-left:1px}\n.foot{display:flex;align-items:center;justify-content:space-between;gap:8px}\n.delta{display:inline-flex;align-items:center;gap:3px;font-size:11.5px;font-weight:600;font-variant-numeric:tabular-nums}\n.delta.up{color:var(--good)}.delta.down{color:var(--bad)}.delta.flat{color:var(--muted)}\n.spark{width:74px;height:24px;flex:0 0 auto}\n.spark path.line{fill:none;stroke:var(--accent);stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}\n.spark path.area{fill:color-mix(in oklab,var(--accent) 14%,transparent);stroke:none}\n.ring{width:26px;height:26px;flex:0 0 auto;transform:rotate(-90deg)}\n.ring circle{fill:none;stroke-width:3.4}\n.ring .bg{stroke:var(--border)}\n.ring .fg{stroke:var(--accent);stroke-linecap:round;transition:stroke-dashoffset .5s ease}\n.target{font-size:10.5px;color:var(--muted);font-variant-numeric:tabular-nums}\n@media (max-width:560px){.wrap{--cols:2 !important}.spark{display:none}}\n@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}\n</style>\n</head>\n<body>\n<div class=\"wrap\" id=\"wrap\"></div>\n<script>\n(function(){\n  var cfgEl=document.getElementById('bk-config'),base={};\n  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];\n  q.forEach(function(v,k){c[k]=v;});\n\n  var ACCENT=c.accent||'#0f7b6c';\n  if(c.theme)document.documentElement.setAttribute('data-theme',c.theme);\n  document.documentElement.style.setProperty('--accent',ACCENT);\n  var wrap=document.getElementById('wrap');\n  wrap.style.setProperty('--cols',String(Math.max(1,Math.min(4,parseInt(c.cols||'4',10)||4))));\n\n  var DEMO=[\n    {label:'MRR',value:48200,unit:'$',prefix:true,delta:8.4,target:60000,series:[31,33,34,36,38,37,40,42,43,45,46,48.2],goodWhen:'up'},\n    {label:'Active users',value:3184,delta:3.1,target:4000,series:[2400,2500,2560,2650,2700,2810,2880,2950,3010,3090,3140,3184],goodWhen:'up'},\n    {label:'Churn',value:2.4,unit:'%',delta:-0.6,target:2.0,series:[3.6,3.5,3.4,3.2,3.3,3.1,2.9,2.8,2.7,2.6,2.5,2.4],goodWhen:'down'},\n    {label:'NPS',value:61,delta:4,target:70,series:[44,46,47,49,50,52,53,55,56,58,59,61],goodWhen:'up'}\n  ];\n  var data=DEMO;\n  if(c.data){\n    try{\n      var parsed=typeof c.data==='string'?JSON.parse(decodeURIComponent(c.data)):c.data;\n      if(Array.isArray(parsed)&&parsed.length)data=parsed;\n    }catch(e){}\n  }\n\n  function fmtNum(n){\n    if(typeof n!=='number'||!isFinite(n))return String(n);\n    var a=Math.abs(n);\n    if(a>=1e6)return (n/1e6).toFixed(a>=1e7?0:1).replace(/\\.0$/,'')+'M';\n    if(a>=1e4)return (n/1e3).toFixed(0)+'k';\n    if(a>=1e3)return n.toLocaleString(undefined,{maximumFractionDigits:0});\n    if(a>=100)return n.toFixed(0);\n    if(a>=10)return n.toFixed(1).replace(/\\.0$/,'');\n    return n.toFixed(2).replace(/\\.0+$/,'').replace(/(\\.\\d*?)0+$/,'$1');\n  }\n  function spark(series,color){\n    if(!Array.isArray(series)||series.length<2)return '';\n    var w=74,h=24,pad=2;\n    var min=Math.min.apply(null,series),max=Math.max.apply(null,series),rng=(max-min)||1;\n    var pts=series.map(function(v,i){\n      var x=pad+(i*(w-pad*2))/(series.length-1);\n      var y=h-pad-((v-min)/rng)*(h-pad*2);\n      return [x,y];\n    });\n    var d=pts.map(function(p,i){return (i?'L':'M')+p[0].toFixed(1)+' '+p[1].toFixed(1);}).join(' ');\n    var area=d+' L'+pts[pts.length-1][0].toFixed(1)+' '+h+' L'+pts[0][0].toFixed(1)+' '+h+' Z';\n    return '<svg class=\"spark\" viewBox=\"0 0 '+w+' '+h+'\" preserveAspectRatio=\"none\" aria-hidden=\"true\">'+\n      '<path class=\"area\" d=\"'+area+'\"></path><path class=\"line\" d=\"'+d+'\"></path></svg>';\n  }\n  function ring(pct){\n    var r=10,circ=2*Math.PI*r,off=circ*(1-Math.max(0,Math.min(1,pct)));\n    return '<svg class=\"ring\" viewBox=\"0 0 26 26\" aria-hidden=\"true\">'+\n      '<circle class=\"bg\" cx=\"13\" cy=\"13\" r=\"'+r+'\"></circle>'+\n      '<circle class=\"fg\" cx=\"13\" cy=\"13\" r=\"'+r+'\" stroke-dasharray=\"'+circ.toFixed(1)+'\" stroke-dashoffset=\"'+off.toFixed(1)+'\"></circle></svg>';\n  }\n  function tile(m){\n    var delta=typeof m.delta==='number'?m.delta:null;\n    var dCls='flat',arrow='→';\n    if(delta!==null){\n      var good=m.goodWhen==='down'?(delta<0):(delta>0);\n      if(delta===0){dCls='flat';arrow='→';}\n      else{dCls=good?'up':'down';arrow=delta>0?'↑':'↓';}\n    }\n    var unit=m.unit||'',prefix=!!m.prefix;\n    var val=(prefix?unit:'')+fmtNum(m.value)+(prefix?'':unit);\n    var pct=(typeof m.target==='number'&&m.target)?m.value/m.target:null;\n    var targetTxt=pct!==null?Math.round(pct*100)+'% of '+fmtNum(m.target):'';\n    return '<div class=\"tile\">'+\n      '<div class=\"head\"><span class=\"name\">'+String(m.label||'').replace(/[<>&]/g,'')+'</span></div>'+\n      '<div class=\"value\">'+val+'</div>'+\n      '<div class=\"foot\">'+\n        '<span class=\"delta '+dCls+'\">'+arrow+(delta!==null?' '+Math.abs(delta).toFixed(1)+'%':'')+'</span>'+\n        spark(m.series)+\n        (pct!==null?ring(pct):'')+\n      '</div>'+\n      (targetTxt?'<div class=\"target\">'+targetTxt+'</div>':'')+\n    '</div>';\n  }\n  wrap.innerHTML=data.slice(0,4).map(tile).join('');\n})();\n</script>\n</body>\n</html>\n"}]}