Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
mention-autocomplete.ts5.5 KB · 101 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
 * @mention autocomplete for comment textareas.
 *
 * Injects a floating dropdown when the user types `@` followed by at least
 * one character. Fetches `/api/users/suggest?q=<prefix>` and renders up to
 * 8 matching usernames. Arrow-key navigation + Enter/Tab to select.
 *
 * Usage: call `mentionAutocompleteScript()` and inject the return value
 * into a `<script dangerouslySetInnerHTML={{ __html: ... }} />` tag inside
 * any page that has comment textareas. The script attaches itself to ALL
 * textareas on the page via event delegation.
 */

export function mentionAutocompleteScript(): string {
  return `(function(){
  var _mc_popup=null,_mc_ta=null,_mc_idx=0,_mc_items=[];
  var _mc_css=[
    '.mc-popup{position:fixed;z-index:9999;background:var(--bg-elevated,#1c2128);border:1px solid var(--border,#30363d);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.40);overflow:hidden;min-width:180px;max-width:280px;}',
    '.mc-item{display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:13px;cursor:pointer;color:var(--text,#e6edf3);}',
    '.mc-item:hover,.mc-item.is-sel{background:var(--bg-hover,#21262d);}',
    '.mc-item-at{color:var(--text-muted,#8b949e);font-size:12px;}',
    '.mc-item-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;}',
  ].join('');
  function injectStyle(){if(!document.getElementById('mc-style')){var s=document.createElement('style');s.id='mc-style';s.textContent=_mc_css;document.head.appendChild(s);}}
  function getCaretCoords(ta){
    var mirror=document.createElement('div');
    var cs=getComputedStyle(ta);
    ['fontFamily','fontSize','fontWeight','letterSpacing','lineHeight','padding','border','boxSizing','whiteSpace','wordWrap','overflowWrap'].forEach(function(p){mirror.style[p]=cs[p];});
    mirror.style.position='absolute';mirror.style.top='-9999px';mirror.style.left='-9999px';mirror.style.width=ta.offsetWidth+'px';mirror.style.height='auto';
    var text=ta.value.slice(0,ta.selectionStart);
    mirror.textContent=text;
    var span=document.createElement('span');span.textContent='@';mirror.appendChild(span);
    document.body.appendChild(mirror);
    var r=ta.getBoundingClientRect();
    var rect=span.getBoundingClientRect();
    document.body.removeChild(mirror);
    return{top:rect.top,left:rect.left};
  }
  function showPopup(ta,items,atPos){
    injectStyle();hidePopup();
    if(!items.length)return;
    _mc_ta=ta;_mc_items=items;_mc_idx=0;
    _mc_popup=document.createElement('div');_mc_popup.className='mc-popup';
    items.forEach(function(u,i){
      var d=document.createElement('div');d.className='mc-item'+(i===0?' is-sel':'');
      d.innerHTML='<span class="mc-item-at">@</span><span class="mc-item-name">'+escHtml(u.username)+'</span>';
      d.addEventListener('mousedown',function(e){e.preventDefault();insertMention(u.username,atPos);});
      _mc_popup.appendChild(d);
    });
    var coords=getCaretCoords(ta);
    _mc_popup.style.top=(coords.top+20)+'px';
    _mc_popup.style.left=coords.left+'px';
    document.body.appendChild(_mc_popup);
  }
  function hidePopup(){if(_mc_popup){_mc_popup.remove();_mc_popup=null;}_mc_ta=null;_mc_items=[];_mc_idx=0;}
  function setSelection(i){_mc_idx=i;var els=_mc_popup?_mc_popup.querySelectorAll('.mc-item'):[];els.forEach(function(el,j){el.classList.toggle('is-sel',j===i);});}
  function insertMention(username,atPos){
    if(!_mc_ta)return;
    var val=_mc_ta.value;var before=val.slice(0,atPos);var after=val.slice(_mc_ta.selectionStart);
    var newVal=before+'@'+username+' '+after;
    _mc_ta.value=newVal;
    var pos=atPos+username.length+2;
    _mc_ta.setSelectionRange(pos,pos);
    hidePopup();
    _mc_ta.dispatchEvent(new Event('input',{bubbles:true}));
  }
  function escHtml(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
  var _mc_timer=null;
  document.addEventListener('input',function(e){
    var ta=e.target;
    if(ta.tagName!=='TEXTAREA')return;
    clearTimeout(_mc_timer);
    var val=ta.value;var pos=ta.selectionStart;
    var lastAt=val.lastIndexOf('@',pos-1);
    if(lastAt===-1){hidePopup();return;}
    var afterAt=val.slice(lastAt+1,pos);
    if(/\\s/.test(afterAt)||afterAt.length===0){hidePopup();return;}
    if(afterAt.length>20){hidePopup();return;}
    var atPos=lastAt;
    _mc_timer=setTimeout(function(){
      fetch('/api/users/suggest?q='+encodeURIComponent(afterAt)).then(function(r){return r.json();}).then(function(data){
        if(!ta.isConnected){hidePopup();return;}
        showPopup(ta,data.users||[],atPos);
      }).catch(function(){hidePopup();});
    },120);
  });
  document.addEventListener('keydown',function(e){
    if(!_mc_popup)return;
    if(e.key==='ArrowDown'){e.preventDefault();setSelection(Math.min(_mc_idx+1,_mc_items.length-1));}
    else if(e.key==='ArrowUp'){e.preventDefault();setSelection(Math.max(_mc_idx-1,0));}
    else if(e.key==='Enter'||e.key==='Tab'){
      if(_mc_popup){e.preventDefault();insertMention(_mc_items[_mc_idx].username,_mc_popup._atPos||0);
      // re-read atPos from last known
      var u=_mc_items[_mc_idx];if(u&&_mc_ta){var val=_mc_ta.value;var p=_mc_ta.selectionStart;var lastAt=val.lastIndexOf('@',p-1);insertMention(u.username,lastAt);}}
    }
    else if(e.key==='Escape'){hidePopup();}
  });
  document.addEventListener('click',function(e){if(_mc_popup&&!_mc_popup.contains(e.target))hidePopup();});
  document.addEventListener('blur',function(e){if(e.target.tagName==='TEXTAREA')setTimeout(function(){if(_mc_popup&&!_mc_popup.matches(':hover'))hidePopup();},150);},true);
})();`;
}