cc23b76a1f2c7dc6a29c227661bfb0ce4d20eb54
lrnassar
  Fri Sep 4 13:43:48 2026 -0700
BLAT page polish from QA: Load example button, corrected input limits, wording fixes. refs #37996

Load example is a chip-style button with a mouseover instead of a hyperlink. Removed the
Show input limits modal; corrected the limits in hgTracksHelp.html, stale since 2013 (DNA is
75,000 bases per sequence and 187,500 combined, protein and translated 10,000 and 25,000),
added a #blatLimits anchor and pointed the two over-limit BLAT warnings and the form at it.
Genome selector: section title Target assembly, type-to-search hint in the dropdown,
Shortcuts: label, input and Query type select sized to match. Similar tools sidebar uses
house-style hyphens and corrects the findMotif name and description. Share and rename text
now states link lifetimes accurately (snapshot links last years, only sessions are
permanent). Alignment page buttons match .gbPill, summary strip spacing fixed, New BLAT
search button demoted from primary. Documents blatResultsGroup in ex.hg.conf.

diff --git src/hg/js/hgBlat.js src/hg/js/hgBlat.js
index 4f86984aaf0..1212770ffcb 100644
--- src/hg/js/hgBlat.js
+++ src/hg/js/hgBlat.js
@@ -282,33 +282,35 @@
 
 // Render the share box.  url set -> show the link + Copy; url null -> "Creating link…"; msg (url null)
 // -> show an error.
 function blatShowShareBox(box, url, msg) {
     box.style.display = 'flex';
     if (msg) {
         box.innerHTML = '<span class="gbShareMsg gbShareFull" style="color:#a00">' +
             htmlEncode(msg) + '</span>';
         return;
     }
     if (!url) {
         box.innerHTML = '<span class="gbShareMsg gbShareFull">Creating link…</span>';
         return;
     }
     box.innerHTML =
-        '<span class="gbShareMsg gbShareFull">Shareable link — anyone with it can reopen these ' +
-        'BLAT results. It stores only the results (not your other tracks or settings) and stays ' +
-        'active as long as it is used.</span>' +
+        '<span class="gbShareMsg gbShareFull">Anyone with this <b>shareable link</b> can open ' +
+        'these BLAT results. It stores only the results, not your other tracks or settings. The ' +
+        'link remains valid for years; to keep your results permanently, save them into a ' +
+        `<a href="hgSession?db=${encodeURIComponent(hgBlatData.config.db || '')}` +
+        `&hgsid=${encodeURIComponent(hgBlatData.config.hgsid || '')}">Session</a>.</span>` +
         '<input id="gbShareInput" class="gbShareInput" type="text" readonly>' +
         '<button type="button" class="gbPill" id="blatShareCopy" title="Copy the link to the clipboard">Copy</button>';
     var inp = document.getElementById('gbShareInput');
     inp.value = url;
     inp.focus();
     inp.select();
     $('#blatShareCopy').on('click', function() {
         inp.select();
         if (navigator.clipboard) { navigator.clipboard.writeText(url); }
         else { document.execCommand('copy'); }
         this.textContent = 'Copied';
     });
 }
 
 function blatShareLink() {
@@ -350,31 +352,31 @@
 // ---- Rename BLAT track (modal) -------------------------------------------
 // The results custom track is built (and renamed) by hgBlat.c's inline code, which exposes a small
 // window.blatRenameCt(name, description) helper (it POSTs to hgc's buildBigPsl and rebuilds the
 // track).  We reuse that helper (no new endpoint), just swapping its old inline toggle-form UI for a
 // proper modal dialog.  The current name/description come from cfg (hgBlat.c), not a global, so this
 // does not depend on any generic page-global.
 
 function blatRenameModalHtml(cfg) {
     // hgSession link is relative (same /cgi-bin/), carrying db + hgsid so the session page opens in
     // this assembly and cart.
     var sessionUrl = `hgSession?db=${encodeURIComponent(cfg.db)}&hgsid=${encodeURIComponent(cfg.hgsid)}`;
     return '<div id="gbModalBg" class="gbModalBg" style="display:none">' +
         '<div class="gbModal" role="dialog" aria-modal="true" aria-labelledby="gbModalTitle">' +
         '<div class="gbModalTitle" id="gbModalTitle">Rename BLAT Track</div>' +
         '<div class="gbModalText">Every BLAT result is stored in its own track in the Genome ' +
-        'Browser. You can rename the track here. Results will disappear after 2–3 days, unless ' +
+        'Browser, which you can rename here. Results will disappear after 2 days unless ' +
         `they are saved into a <a href="${sessionUrl}">Session link</a>.</div>` +
         '<label class="gbModalLabel" for="blatRenameName">Track name</label>' +
         '<input id="blatRenameName" class="gbModalInput" type="text" maxlength="80">' +
         '<label class="gbModalLabel" for="blatRenameDesc">Description</label>' +
         '<input id="blatRenameDesc" class="gbModalInput" type="text" maxlength="120">' +
         '<div class="gbModalBtns">' +
         '<button type="button" class="gbPill" id="blatRenameCancel">Cancel</button>' +
         '<button type="button" class="gbPill primary" id="blatRenameOk">OK</button>' +
         '</div></div></div>';
 }
 
 function blatCloseRename() {
     var bg = document.getElementById('gbModalBg');
     if (bg) { bg.style.display = 'none'; }
 }
@@ -477,31 +479,31 @@
     // render too so the dialog shows the full server+client picture.
     var tBuildStart = (hgBlatData.timing && window.performance) ? performance.now() : 0;
 
     // Pin a stable, shareable URL into the address bar (no server redirect) so refresh, bookmark and
     // "Share a link" all use the trash-backed reopen link instead of the transient POST/search URL.
     if (cfg.shareUrl) {
         try { history.replaceState(null, '', cfg.shareUrl); } catch (e) { /* older browsers: ignore */ }
     }
 
     var back = cfg.backUrl ?
         `<a class="gbPill" title="Return to the Genome Browser at your previous location (${htmlEncode(cfg.backPos)})" ` +
         `href="${htmlEncode(cfg.backUrl)}">Back to Genome Browser</a>` : '';
     // The page actions live in the gold main-header bar (framework #sectTtl), next to the title -
     // so there is no separate toolbar (.blatHead is gone).  Injected into #sectTtl below.
     var headActions =
-        `${back}<a class="gbPill primary" title="Start a new BLAT search" href="${htmlEncode(cfg.newSearchUrl)}">New BLAT search</a>`;
+        `${back}<a class="gbPill" title="Start a new BLAT search" href="${htmlEncode(cfg.newSearchUrl)}">New BLAT search</a>`;
 
     // Top banner: note this is the new page, link back to the classic page (fresh searches only,
     // where the trash files still exist), and invite feedback.  The old page also clears the
     // blatNewPage preference so later searches use the classic page until the user opts back in.
     var origPage = cfg.canOldPage ?
         ` You can go back to <a title="Show these results on the classic BLAT results page" ` +
         `href="hgBlat?blatNewPage=0&blatReopen=1&hgsid=${encodeURIComponent(cfg.hgsid)}">the original page</a> anytime.` : '';
     var bannerHtml =
         `<div class="gbBanner">We are testing a new BLAT output page.${origPage} ` +
         `If you have feedback on this new page, do not hesitate to let us know via ` +
         `<a href="mailto:genome@soe.ucsc.edu">genome@soe.ucsc.edu</a>.</div>`;
 
     var queryCount = new Set(hits.map(h => h.qName)).size;
 
     var th = [];
@@ -687,58 +689,44 @@
 }
 
 function blatOpts(list, cur) {
     return list.map(function(v) {
         return `<option value="${htmlEncode(v)}"${v === cur ? ' selected' : ''}>${htmlEncode(v)}</option>`;
     }).join('');
 }
 
 function blatFormCount() {
     // Live character count under the textarea.  Only these two nodes are touched on input - the
     // textarea itself is never re-rendered, so the caret stays where the user put it.
     var ta = document.getElementById('blatUserSeq');
     var out = document.getElementById('blatCountText');
     if (!ta || !out) { return; }
     var n = ta.value.replace(/[^A-Za-z*]/g, '').length;
-    out.textContent = blatFmt(n) + ' of 25,000 characters';
-    $('#blatLimitLink').toggleClass('over', n > 25000);
+    // 75,000 is the DNA per-sequence limit (hgBlat.c maxSingleSize); protein and translated
+    // queries top out at 10,000, but the server rejects those with a warning that links to the
+    // full limits in the docs, so the counter shows only the common case.
+    out.textContent = blatFmt(n) + ' of 75,000 characters';
+    out.classList.toggle('over', n > 75000);
 }
 
 function blatFormTab(showUpload) {
     $('#blatTabPaste').toggleClass('on', !showUpload);
     $('#blatTabUpload').toggleClass('on', showUpload);
     $('#blatPanePaste').toggle(!showUpload);
     $('#blatPaneUpload').toggle(showUpload);
 }
 
-function blatFormLimitsModal() {
-    var row = (k, v) => `<div class="blatLimitRow"><span>${k}</span><strong>${v}</strong></div>`;
-    return '<div id="blatLimitsBg" class="gbModalBg" style="display:none">' +
-        '<div class="gbModal" role="dialog" aria-modal="true" aria-labelledby="blatLimitsTitle">' +
-        '<div class="gbModalTitle" id="blatLimitsTitle">Input limits</div>' +
-        row('DNA per sequence', '25,000 bases') +
-        row('Protein / translated', '10,000 letters') +
-        row('Sequences per run', '25') +
-        row('Total per submission', '50,000 bases') +
-        '<div class="gbModalText gbModalNote">Queries above these limits are rejected ' +
-        'before alignment. For larger jobs, run BLAT from the ' +
-        '<a target="_blank" href="https://hgdownload.soe.ucsc.edu/downloads.html#utilities_downloads">' +
-        'command line</a> on your own server.</div>' +
-        '<div class="gbModalBtns"><button type="button" class="gbPill" id="blatLimitsClose">Close</button></div>' +
-        '</div></div>';
-}
-
 function blatFormBusyMarkup() {
     // Spinner overlay shown between Submit and the arrival of the results page.  Built with the
     // form (hidden) rather than on demand, so nothing has to be parsed or fetched at the moment the
     // browser is already busy navigating away.
     return '<div id="blatBusyBg" class="gbBusyBg" role="status" aria-live="polite">' +
         '<div class="gbBusyCard"><div class="gbSpinner"></div>' +
         '<div><div class="gbBusyTitle" id="blatBusyTitle">Running BLAT&hellip;</div>' +
         '<div class="gbBusyText" id="blatBusyText"></div></div></div></div>';
 }
 
 function blatFormBusyWire(cfg) {
     // The search is a plain form POST that navigates the page, so between the click on Submit and
     // the arrival of the results nothing on screen changes at all - and a BLAT run is anywhere from
     // under a second to well over half a minute, the long end being an assembly served by a dynamic
     // BLAT server, which has to load its index from disk before it can answer.  Cover the form with
@@ -808,39 +796,40 @@
     document.mainForm.db.value = db;
     $('#blatFormBox a[data-urltpl]').each(function() {
         this.href = this.getAttribute('data-urltpl').replace('$DB$', encodeURIComponent(db));
     });
 }
 
 function blatFormSidebar(cfg) {
     // Same links the classic page offered.  hgBlat.c supplies them as templates holding $DB$ (see
     // blatFormSetDb); the template is kept in data-urltpl so the link can be retargeted later.
     var tools = '';
     var tplLink = (tpl, label) => {
         var href = tpl.replace('$DB$', encodeURIComponent(hgBlatFormData.db));
         return `<a data-urltpl="${htmlEncode(tpl)}" href="${htmlEncode(href)}">${label}</a>`;
     };
     if (cfg.pcrUrlTpl) {
-        tools += `<div>${tplLink(cfg.pcrUrlTpl, 'In-Silico PCR')} — better than BLAT for ` +
+        tools += `<div>${tplLink(cfg.pcrUrlTpl, 'In-Silico PCR')} - better than BLAT for ` +
             'locating PCR primers.</div>';
     }
     if (cfg.oligoMatchUrlTpl) {
-        tools += `<div>${tplLink(cfg.oligoMatchUrlTpl, 'Short Sequence Match')} — for ` +
+        tools += `<div>${tplLink(cfg.oligoMatchUrlTpl, 'Short Sequence Match')} - for ` +
             'sequences under 20 bp, within the region shown in the Genome Browser.</div>';
     }
     tools += '<div><a target="_blank" href="https://hgdownload.soe.ucsc.edu/downloads.html#utilities_downloads">' +
-        'findMotifs</a> — command-line search across a whole genome.</div>';
+        'findMotif</a> - like Short Sequence Match, but searches a whole genome, ' +
+        'from the command line.</div>';
     return '<div>' +
         (tools ? `<div class="gbCard"><h3>Similar tools</h3>${tools}</div>` : '') +
         '<div class="gbCard"><h3>Help</h3>' +
         '<div><a href="../FAQ/FAQblat.html">BLAT FAQ</a></div>' +
         '<div><a href="../goldenPath/help/hgTracksHelp.html#BLATAlign">BLAT documentation</a></div>' +
         // No "Search all genomes FAQ" here: that link now lives in the "Search many genomes"
         // tooltip, next to the checkbox it actually explains.
         '<div><a href="../FAQ/FAQblat.html#blat14">Programmatic / batch BLAT</a></div>' +
         '</div>' +
         '<div class="gbCard"><h3>About BLAT</h3>' +
         '<div>DNA BLAT quickly finds sequences of 95% and greater similarity that are at least 25 bases ' +
         'long; it finds perfect matches down to 20 bases, and may miss shorter or more divergent ' +
         'alignments. Protein BLAT finds sequences of 80% and greater similarity at least 20 amino acids ' +
         'long.</div>' +
         '<div>Kent WJ. <a target="_blank" href="https://genome.cshlp.org/content/12/4/656.abstract">' +
@@ -876,31 +865,31 @@
     var keepResultsInit = cfg.keepResults;
     if (cfg.showKeepResults) {
         var storedKeep = blatGetKeepResultsPref();
         if (storedKeep !== null) { keepResultsInit = storedKeep; }
     }
     var onlyLatestInit = cfg.onlyLatest;
     if (cfg.showOnlyLatest) {
         var storedLatest = blatGetOnlyLatestPref();
         if (storedLatest !== null) { onlyLatestInit = storedLatest; }
     }
 
     document.getElementById('blatFormBox').innerHTML =
         banner +
         '<div class="blatFormGrid"><div>' +
 
-        '<div class="gbSection">Search &ndash; type keywords to find the target assembly</div>' +
+        '<div class="gbSection">Target assembly</div>' +
         '<div class="blatRow">' +
             '<div class="blatField blatGenomeSlot"><span>Genome or assembly ' +
                 `<span class="blatInfo" title="${BLAT_GENOME_SEARCH_HELP}">${BLAT_INFO_SVG}</span>` +
                 '</span>' +
                 '<div id="blatGenomeSlot"></div></div>' +
             `<label class="blatField"><span>Query type</span><select name="type">${blatOpts(cfg.types, cfg.type)}</select></label>` +
             // Sort and output are submitted but not offered: sorting by anything other than score
             // is rarely useful, and this page always wants the hyperlink (results table) output.
             // Kept as hidden fields so the request hgBlat receives is unchanged.
             `<input type="hidden" name="sort" value="${htmlEncode(cfg.sort)}">` +
             `<input type="hidden" name="output" value="${htmlEncode(cfg.output)}">` +
         '</div>' +
 
         '<div class="blatChecks">' +
             // The mouseover popup keeps itself open while the pointer is inside it (see the
@@ -949,58 +938,57 @@
                     'Each new BLAT search removes your earlier BLAT result tracks, so only the ' +
                     'newest search shows in the Genome Browser. Leave unchecked to keep all ' +
                     'results. Your choice is remembered for next time.')}">` +
                 `${BLAT_INFO_SVG}</span></span>` : '') +
         '</div>' +
 
         '<div class="gbSection">Query sequence</div>' +
         '<div class="blatTabs">' +
             '<button type="button" class="blatTab on" id="blatTabPaste">Paste sequence</button>' +
             '<button type="button" class="blatTab" id="blatTabUpload">Upload file</button>' +
         '</div>' +
 
         '<div id="blatPanePaste">' +
             '<div class="blatPaneHint"><span>Separate multiple sequences with a &gt;name line. ' +
             'Up to 25 sequences.</span>' +
-            `<a href="#" id="blatExample">${htmlEncode(cfg.exampleLabel)}</a></div>` +
+            `<button type="button" id="blatExample" class="blatChip" ` +
+            `title="${htmlEncode(cfg.exampleTitle)}">${htmlEncode(cfg.exampleLabel)}</button></div>` +
             '<textarea class="blatSeq" name="userSeq" id="blatUserSeq" spellcheck="false" ' +
             'aria-label="Paste in a query sequence"></textarea>' +
-            '<div class="blatCount"><span id="blatCountText"></span>' +
-            '<a href="#" id="blatLimitLink">Show input limits</a></div>' +
+            '<div class="blatCount"><span id="blatCountText"></span></div>' +
         '</div>' +
 
         '<div id="blatPaneUpload" style="display:none">' +
             '<div class="blatDrop" id="blatDrop">' +
                 '<div class="blatDropTitle">Drop a sequence file here</div>' +
-                '<div class="blatDropSub">Plain text or FASTA, up to 50,000 bases total</div>' +
+                '<div class="blatDropSub">Plain text or FASTA, up to 25 sequences</div>' +
                 '<input type="file" name="seqFile" id="blatSeqFile">' +
                 '<div class="blatFileName" id="blatFileName"></div>' +
             '</div>' +
         '</div>' +
 
         '<div class="blatActions">' +
             '<input type="submit" class="gbPill primary" name="Submit" value="Submit" ' +
             `title="${htmlEncode('Align the sequence and show all matches')}">` +
             '<input type="submit" class="gbPill" name="Lucky" value="I&#39;m feeling lucky" ' +
             `title="${htmlEncode('Skip the list of matches and open the best-scoring one straight ' +
-                'away in the Genome Browser. Ignored when "Search many genomes" is ticked.')}">` +
+                'away in the Genome Browser. Ignored when "Search many genomes" is checked.')}">` +
             '<input type="submit" class="gbPill" name="Clear" value="Clear" ' +
             `title="${htmlEncode('Empty the query sequence box')}">` +
         '</div>' +
 
         '</div>' + blatFormSidebar(cfg) + '</div>' +
-        blatFormLimitsModal() +
         blatFormBusyMarkup();
 
     // Move the C-generated genome search bar (real autocomplete over every assembly, already wired
     // by setupGenomeSearchBar) into its slot, rather than reimplementing it with a hardcoded list.
     var holder = document.getElementById('blatGenomeHolder');
     if (holder) { document.getElementById('blatGenomeSlot').appendChild(holder); }
 
 
     // Show the current assembly in the search bar itself instead of in a separate "Current genome:"
     // line - the bar is wide enough for the whole description.  setupGenomeSearchBar writes the new
     // one in on each pick, and focusing the bar selects all of it, so it reads as a filled-in search
     // box rather than as a value the user has to clear by hand.
     var genomeInput = document.getElementById('genomeSearch');
     if (genomeInput && cfg.dbLabel) { genomeInput.value = cfg.dbLabel; }
 
@@ -1011,67 +999,60 @@
     $('#blatUserSeq').on('input', blatFormCount);
     // Mirror the "Keep results" checkbox into its hidden field so an unticked box submits an
     // explicit 0 rather than nothing at all, and remember the choice in localStorage so it comes
     // back pre-set on the user's next visit (see keepResultsInit above).
     $('#blat_keepResults').on('change', function() {
         document.getElementById('blatKeepResultsVal').value = this.checked ? '1' : '0';
         blatSetKeepResultsPref(this.checked);
     });
     $('#blat_onlyLatest').on('change', function() {
         document.getElementById('blatOnlyLatestVal').value = this.checked ? '1' : '0';
         blatSetOnlyLatestPref(this.checked);
     });
     $('#blatTabPaste').on('click', function() { blatFormTab(false); });
     $('#blatTabUpload').on('click', function() { blatFormTab(true); });
     // The example sequence is a real 2.5 kb query, fetched on demand so it is not carried in every
-    // page load.  The link doubles as its own status indicator while the request is in flight.
-    $('#blatExample').on('click', function(ev) {
-        ev.preventDefault();
+    // page load.  The button doubles as its own status indicator while the request is in flight,
+    // and is disabled for that interval so a double-click cannot start a second fetch.
+    $('#blatExample').on('click', function() {
         var link = this;
         var label = cfg.exampleLabel;
+        link.disabled = true;
         link.textContent = 'Loading example…';
         fetch(cfg.exampleUrl)
             .then(function(resp) {
                 if (!resp.ok) { throw new Error('HTTP ' + resp.status); }
                 return resp.text();
             })
             .then(function(fa) {
                 var ta = document.getElementById('blatUserSeq');
                 ta.value = fa.trim();
                 blatFormCount();
                 ta.focus();
                 ta.setSelectionRange(0, 0);
                 ta.scrollTop = 0;
                 link.textContent = label;
+                link.disabled = false;
                 blatFormTab(false);   // in case the user was on the upload tab
             })
             .catch(function(err) {
                 link.textContent = 'Could not load example';
                 // Leave the message up briefly, then let the user try again.
-                setTimeout(function() { link.textContent = label; }, 4000);
+                setTimeout(function() { link.textContent = label; link.disabled = false; }, 4000);
                 console.error('hgBlat: example fetch failed:', err);
             });
     });
-    $('#blatLimitLink').on('click', function(ev) {
-        ev.preventDefault();
-        $('#blatLimitsBg').css('display', 'flex');
-    });
-    $('#blatLimitsClose').on('click', function() { $('#blatLimitsBg').hide(); });
-    $('#blatLimitsBg').on('click', function(ev) { if (ev.target === this) { $(this).hide(); } });
-    $(document).on('keydown.blatLimits', function(ev) {
-        if (ev.key === 'Escape') { $('#blatLimitsBg').hide(); }
-    });
 
     var fileInput = document.getElementById('blatSeqFile');
     var drop = document.getElementById('blatDrop');
     $(fileInput).on('change', function() {
         document.getElementById('blatFileName').textContent =
             this.files && this.files.length ? this.files[0].name : '';
     });
     ['dragenter', 'dragover'].forEach(function(e) {
         drop.addEventListener(e, function(ev) { ev.preventDefault(); drop.classList.add('hot'); });
     });
     ['dragleave', 'drop'].forEach(function(e) {
         drop.addEventListener(e, function(ev) { ev.preventDefault(); drop.classList.remove('hot'); });
     });
     drop.addEventListener('drop', function(ev) {
         if (ev.dataTransfer.files.length) {