9f8d33c8b2b6bc61f6d02d781c4e02836f7099f9
max
  Fri Aug 21 02:05:42 2026 -0700
hgSession: new opt-in JavaScript "My Sessions" page; share gbModern.css with hgBlat. refs #38157

Applies the hgBlat facelift strategy (#37996) to hgSession: an opt-in,
client-rendered "My Sessions" page gated by the sessionNewPage /
sessionNewPageBanner hg.conf flags (mirroring blatNewForm / blatNewFormBanner),
with a banner linking between the classic and new pages so neither is a one-way
door. sessionNewPage also flips the site default.

hgSession.c stays the data/action backend: it emits the session list and page
config as an inline JSON global (hgSessionData) into an empty #sessionApp
container, and the new hgSession.js builds the UI - a save-current-view card
(name + optional description + "only I can load it"; empty name saves under a
random share_ name), a "most recently saved session" one-click Update, a
searchable/sortable/paged DataTable of sessions (assembly + position, created
with last-used on hover, views, a lock icon on private sessions), inline Share
(copy link / email / gallery), Edit (rename + description + private), Overwrite
and Delete, and a bulk Select -> Delete-all-selected mode.  The mutating actions
POST to new JSON endpoints (hgS_doDeleteJson / doShareJson / doGalleryJson /
doOverwriteJson / doDescribeJson) that run the same SQL as the classic full-page
handlers and return JSON, so the table updates in place; loads, file up/downloads
and custom-track backup stay as ordinary form submits/links.  The Advanced panel
keeps feature parity with the classic page (load another user's session, load
from URL/file, save to file, back up custom tracks, reset), minus the login/
change-password links that now live in the top menu.

Shared UCSC house-style components (design tokens, .gbPill, .gbCard, .gbStrip,
.gbSection, .gbShareBox, .gbBanner, the .gbModal* dialog and a .gbTable) are
factored into a new gbModern.css.  hgBlat is migrated onto it: its generic
.blat* classes are renamed to the shared .gb* names in hgBlat.css / hgBlat.js
and the #blatResults / #blatFormBox containers get class="gbApp"; verified
pixel-clean against the previous search form and results pages, including the
rename modal.  hgSession.css holds only session-specific layout.

diff --git src/hg/htdocs/style/hgSession.css src/hg/htdocs/style/hgSession.css
new file mode 100644
index 00000000000..518617ccdf0
--- /dev/null
+++ src/hg/htdocs/style/hgSession.css
@@ -0,0 +1,100 @@
+/* hgSession.css - session-specific layout for the modern "My Sessions" page (hgSession.js).
+ *
+ * The shared UCSC house-style components (tokens, .gbPill, .gbBanner, .gbCard, .gbModal*,
+ * .gbSection, .gbShareBox, .gbTable) live in gbModern.css, loaded alongside this file via
+ * webIncludeResourceFile.  This file only holds the pieces unique to the sessions page: the
+ * save-current-view card, the session table's columns, and the Advanced panel. */
+
+/* The whole app sits in #sessionApp, which also carries class "gbApp" for the design tokens. */
+#sessionApp { margin:12px 0 24px; }
+#sessionApp .sessIntro { font-size:14px; color:var(--ink); margin:0 0 14px; }
+#sessionApp .sessIntro a { color:var(--accent); }
+#sessionApp .sessIntro a:hover { color:var(--accentHover); }
+
+/* Save-current-view card - the primary action.  Neutral light-grey panel. */
+#sessionApp .sessSaveCard { border:0; background:#f6f8fa; padding:14px 16px;
+    display:flex; flex-direction:column; gap:12px; margin:0 0 20px; }
+#sessionApp .sessSaveHead { display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; }
+/* Title and the current-view summary share one font size (title is just bold). */
+#sessionApp .sessSaveTitle { font-size:14px; font-weight:700; color:var(--accent); }
+#sessionApp .sessSaveWhat { font-size:14px; }
+#sessionApp .sessSaveRow { display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
+#sessionApp .sessSaveInput { flex:1 1 320px; min-width:220px; height:34px; box-sizing:border-box;
+    padding:0 11px; font-size:14px; border:1px solid var(--btnLine); border-radius:0; background:#fff;
+    font-family:inherit; }
+/* The optional description input spans the card on its own line below the name row. */
+#sessionApp #sessSaveDesc { width:100%; flex:none; }
+#sessionApp .sessSaveCheck { display:flex; align-items:center; gap:7px; font-size:13px;
+    color:var(--ink); white-space:nowrap; }
+
+/* One-click "update the most recently saved session" - a compact thin bar above the save card. */
+#sessionApp .sessRecent { display:flex; align-items:center; gap:8px; flex-wrap:wrap;
+    border:0; background:#f6f8fa; padding:6px 12px; font-size:14px; margin:0 0 12px; }
+#sessionApp .sessRecent .gbPill { padding:2px 10px; }
+#sessionApp .sessRecentTime { color:var(--muted); }
+/* Info bubble (navy circled i) used for the recent-update hint and session descriptions. */
+#sessionApp .sessInfo { color:var(--accent); cursor:help; font-size:15px; line-height:1; }
+
+/* Section heading ("Your saved sessions") separating the save card from the table. */
+#sessionApp .gbSection { margin-top:4px; }
+
+/* Toolbar above the table: DataTables search box + count + import/export. */
+#sessionApp .sessToolbar { display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin:0 0 10px; }
+#sessionApp .sessCount { font-size:13px; color:var(--muted); white-space:nowrap; }
+#sessionApp .sessToolbarActions { margin-left:auto; display:flex; gap:8px; align-items:center; }
+#sessionApp #sessionAppTable_length { margin:0; font-size:14px; color:var(--ink); }
+#sessionApp #sessionAppTable_length select { border:1px solid var(--btnLine); border-radius:0;
+    padding:4px 6px; font-size:14px; background:#fff; margin:0 4px; }
+#sessionApp #sessionAppTable_filter { margin:0; }
+#sessionApp #sessionAppTable_filter input { width:280px; max-width:55vw; border:1px solid var(--btnLine);
+    border-radius:0; padding:5px 9px; font-size:14px; }
+#sessionApp #sessionAppTable_wrapper { overflow-x:auto; }
+
+/* Session table columns.  Left-align every header and cell (including Actions) for one clean edge. */
+#sessionAppTable thead th, #sessionAppTable tbody td { text-align:left; }
+/* Bulk-select checkbox column (hidden until "Select" is pressed). */
+#sessionAppTable td.sessSelCol, #sessionAppTable th.sessSelCol { width:26px; text-align:center; }
+#sessionAppTable td.sessNameCol a { font-weight:700; }
+/* Saved position, shown inline after the assembly. */
+#sessionAppTable .sessPos { font-size:12px; color:var(--muted); white-space:nowrap; }
+#sessionAppTable td.sessDescCol { max-width:280px; white-space:nowrap; overflow:hidden;
+    text-overflow:ellipsis; color:var(--muted); }
+#sessionAppTable td.sessActionsCol { white-space:nowrap; }
+/* All action controls (text and icon-only) share one height so they line up. */
+#sessionAppTable td.sessActionsCol .gbPill { display:inline-flex; align-items:center;
+    justify-content:center; height:24px; padding:0 9px; font-size:12px; margin-left:5px;
+    box-sizing:border-box; vertical-align:middle; }
+#sessionAppTable td.sessActionsCol .gbPill.danger { padding:0 8px; }
+#sessionAppTable .sessIcon { width:13px; height:14px; display:block; }
+#sessionAppTable .sessShareIcon { vertical-align:-2px; margin-right:4px; }
+#sessionAppTable .sessPublic { display:inline-block; margin-left:6px; font-size:11px; font-weight:700;
+    color:#1f7a34; }
+/* Lock marks a private (not-shared) session; sharing is the default so only the exception is marked. */
+#sessionAppTable .sessLockWrap { margin-left:6px; cursor:help; }
+#sessionAppTable .sessLock { width:10px; height:12px; display:inline-block; vertical-align:-2px;
+    color:var(--muted); }
+
+/* Advanced collapsible - the rarer load/backup/reset options.  Collapsed it is a thin bar; sits
+ * between the save card and the sessions table. */
+#sessionApp .sessAdv { border:0; margin:0 0 20px; }
+#sessionApp .sessAdvHead { display:flex; align-items:center; gap:9px; padding:11px 14px;
+    background:#f6f8fa; cursor:pointer; font-size:14px; font-weight:700; color:var(--accent);
+    user-select:none; }
+#sessionApp .sessAdvHead .caret { width:12px; display:inline-block; }
+#sessionApp .sessAdvBody { padding:16px 16px; display:grid; grid-template-columns:1fr 1fr;
+    gap:18px 28px; background:#f6f8fa; font-size:14px; }
+@media (max-width:820px) { #sessionApp .sessAdvBody { grid-template-columns:1fr; } }
+#sessionApp .sessAdvItem { display:flex; flex-direction:column; gap:7px; }
+#sessionApp .sessAdvItem > .lab { font-weight:700; color:var(--ink); }
+#sessionApp .sessAdvRow { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
+#sessionApp .sessAdvInput { flex:1 1 140px; min-width:110px; height:32px; box-sizing:border-box;
+    border:1px solid var(--btnLine); border-radius:0; padding:0 9px; font-size:14px; background:#fff;
+    font-family:inherit; }
+#sessionApp .sessAdvLinks { display:flex; gap:16px; flex-wrap:wrap; }
+#sessionApp .sessAdvLinks a { color:var(--accent); }
+#sessionApp .sessAdvLinks a:hover { color:var(--accentHover); }
+
+/* Small inline status line for AJAX feedback (errors in maroon). */
+#sessionApp .sessMsg { font-size:13px; margin:0 0 12px; }
+#sessionApp .sessMsg.err { color:#b1301f; font-weight:700; }
+#sessionApp .sessMsg.ok { color:#1f7a34; }