My own OCaml monorepo using monopam
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

tweaks

+194 -16
+17 -5
lib/cmd/build.ml
··· 1534 1534 Buffer.contents buf 1535 1535 in 1536 1536 Oi.Cache.Logs.write ~fs ~cache_root log_path body; 1537 + let missing_names = 1538 + OpamSysPkg.Set.elements missing |> List.map OpamSysPkg.to_string 1539 + in 1540 + let n = List.length missing_names in 1541 + (* Cap the inline list at 8 names so a 30-package depext 1542 + set doesn't blow out the warning line; the full list is 1543 + always in [log_path] for follow-up. *) 1544 + let max_inline = 8 in 1545 + let shown, suffix = 1546 + if n <= max_inline then (missing_names, "") 1547 + else 1548 + ( List.filteri (fun i _ -> i < max_inline) missing_names, 1549 + Fmt.str ", … +%d more" (n - max_inline) ) 1550 + in 1537 1551 Log.warn (fun m -> 1538 1552 m 1539 - "%s: opam reports %d missing system package(s); proceeding \ 1540 - with the build anyway. See %s" 1541 - group_targets 1542 - (OpamSysPkg.Set.cardinal missing) 1543 - log_path) 1553 + "%s: opam reports %d missing system package(s) [%s%s]; \ 1554 + proceeding with the build anyway. See %s" 1555 + group_targets n (String.concat ", " shown) suffix log_path) 1544 1556 | None -> ()); 1545 1557 (* After the build, any package in this group's plan whose 1546 1558 layer hash is in [failed_layers] with a non-empty path
+177 -11
registry/index.html
··· 427 427 letter-spacing: 0.04em; 428 428 } 429 429 .group-header .group-count { color: var(--fg-dim); margin-left: 0.4rem; font-weight: normal; } 430 - .overlay-tag { color: var(--accent); font-size: 0.72rem; } 431 - .overlay-tag.none { color: var(--fg-faint); } 430 + /* Default colouring is overridden per-handle via inline [style] from 431 + [handleStyle] — the [color]/[background]/[border-color] declared 432 + here are the no-handle fallback (used by the [.none] variant). */ 433 + .overlay-tag { 434 + color: var(--accent); 435 + font-size: 0.72rem; 436 + display: inline-block; 437 + padding: 0.05rem 0.4rem; 438 + border-radius: 3px; 439 + border: 1px solid transparent; 440 + line-height: 1.4; 441 + white-space: nowrap; 442 + } 443 + .overlay-tag.none { 444 + color: var(--fg-faint); 445 + background: transparent; 446 + border-color: transparent; 447 + padding: 0; 448 + } 432 449 /* Dropdown for the multi-handle case: when an entry's [callers[]] 433 450 names more than one overlay, the row would otherwise overflow the 434 451 ~120px overlay column. Show the first chip plus a "+N" counter that ··· 487 504 .handle-dd-list { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); } 488 505 } 489 506 .pkg-row:hover { background: var(--bg-sunken); } 490 - .pkg-row.expanded { background: var(--bg-sunken); } 507 + /* The handle-marker stripe lives in [box-shadow]; clear it when 508 + [.expanded] / [.upstream] take over the same slot via ::before. */ 509 + .pkg-row.expanded { background: var(--bg-sunken); box-shadow: none !important; } 491 510 .pkg-row.expanded::before { content: ""; width: 3px; background: var(--accent); position: absolute; left: 0; top: 0; bottom: 0; } 492 - .pkg-row.upstream { background: rgba(198, 40, 40, 0.12); } 511 + .pkg-row.upstream { background: rgba(198, 40, 40, 0.12); box-shadow: none !important; } 493 512 .pkg-row.upstream::before { content: ""; width: 3px; background: var(--fail); position: absolute; left: 0; top: 0; bottom: 0; } 494 513 @media (prefers-color-scheme: dark) { 495 514 .pkg-row.upstream { background: rgba(239, 83, 80, 0.18); } ··· 909 928 return [...set].sort(); 910 929 } 911 930 931 + // Palette of (hue, saturation, lightness) triples. The hue ladder is 932 + // hand-laid so consecutive entries sit ~30° apart on the colour wheel 933 + // — distinct enough that adjacent slots never look like the same 934 + // colour family. Sat/lit rotate through two tiers so even doubling 935 + // back through hues produces a recognisably different chip. 936 + const HANDLE_PALETTE = [ 937 + [ 0, 70, 58 ], /* red */ 938 + [ 200, 70, 55 ], /* sky blue */ 939 + [ 35, 80, 55 ], /* tangerine */ 940 + [ 270, 60, 60 ], /* purple */ 941 + [ 130, 55, 45 ], /* forest green */ 942 + [ 320, 60, 58 ], /* magenta */ 943 + [ 60, 65, 50 ], /* mustard */ 944 + [ 230, 65, 60 ], /* royal blue */ 945 + [ 165, 65, 42 ], /* teal */ 946 + [ 295, 50, 55 ], /* orchid */ 947 + [ 18, 75, 55 ], /* coral */ 948 + [ 100, 50, 45 ], /* leaf green */ 949 + [ 250, 55, 65 ], /* periwinkle */ 950 + [ 345, 65, 60 ], /* rose */ 951 + [ 180, 60, 45 ], /* aqua */ 952 + [ 85, 55, 50 ], /* lime */ 953 + [ 215, 55, 50 ], /* azure */ 954 + [ 50, 70, 55 ], /* amber */ 955 + ]; 956 + 957 + // Per-render handle → palette-slot map. Built lazily by [handleHsl] 958 + // when the cache is empty: collects every handle present in the 959 + // active manifest, sorts alphabetically, then walks the palette so 960 + // the assignment is stable for a given (set-of-handles, palette). 961 + // 962 + // The deterministic-by-set approach guarantees ZERO collisions among 963 + // handles that appear together — switching from a hash-based scheme 964 + // where two handles could randomly land on the same slot. 965 + let _handleColorCache = null; 966 + 967 + function invalidateHandleColors() { _handleColorCache = null; } 968 + 969 + function buildHandleColors() { 970 + const set = new Set(); 971 + for (const m of state.manifests.values()) { 972 + for (const r of m.results || []) { 973 + for (const c of r.callers || []) { 974 + if (c.overlay && c.overlay.handle) set.add(c.overlay.handle); 975 + } 976 + } 977 + } 978 + const handles = [...set].sort(); 979 + const map = new Map(); 980 + handles.forEach((h, i) => { 981 + map.set(h, HANDLE_PALETTE[i % HANDLE_PALETTE.length]); 982 + }); 983 + return map; 984 + } 985 + 986 + // "(none)" stays neutral; everything else picks up its deterministic 987 + // slot from the per-set assignment table. 988 + function handleHsl(name) { 989 + if (!name || name === "(none)") return null; 990 + if (!_handleColorCache) _handleColorCache = buildHandleColors(); 991 + return _handleColorCache.get(name) || null; 992 + } 993 + 994 + // Inline style for a handle pill: muted pastel background + matched 995 + // accent text. Uses CSS [color-mix] so the same hue reads correctly in 996 + // both light and dark themes (mixing against [--bg] auto-adapts). 997 + // "(none)" is rendered transparent so unaffiliated rows blend in. 998 + function handleStyle(name) { 999 + const hsl = handleHsl(name); 1000 + if (!hsl) return ""; 1001 + const [h, s, l] = hsl; 1002 + return `--handle-hue:${h}; ` + 1003 + `background: color-mix(in srgb, hsl(${h} ${s}% ${l}%) 22%, transparent); ` + 1004 + `color: color-mix(in oklab, hsl(${h} ${s}% ${Math.max(l - 18, 28)}%) 80%, var(--fg)); ` + 1005 + `border-color: color-mix(in srgb, hsl(${h} ${s}% ${l}%) 45%, transparent);`; 1006 + } 1007 + 1008 + // Per-row handle marker: a thin coloured stripe along the leading 1009 + // edge. Skipped when the row is [.expanded] or [.upstream] (those 1010 + // classes already paint the same 3px slot via ::before with a more 1011 + // important meaning). We use [box-shadow] rather than [background] so 1012 + // the [:hover] / [.expanded] background rules still win. 1013 + function handleRowStyle(handles) { 1014 + const primary = (handles || []).find(h => h && h !== "(none)"); 1015 + if (!primary) return ""; 1016 + const [h, s, l] = handleHsl(primary); 1017 + return `box-shadow: inset 3px 0 0 hsl(${h} ${s}% ${l}% / 0.55);`; 1018 + } 1019 + 1020 + // Group-header tint for [Sort: by overlay] mode. 1021 + function handleHeaderStyle(name) { 1022 + const hsl = handleHsl(name); 1023 + if (!hsl) return ""; 1024 + const [h, s, l] = hsl; 1025 + return `background: color-mix(in srgb, hsl(${h} ${s}% ${l}%) 18%, var(--bg-sunken)); ` + 1026 + `color: color-mix(in oklab, hsl(${h} ${s}% ${Math.max(l - 20, 28)}%) 80%, var(--fg)); ` + 1027 + `border-left: 4px solid hsl(${h} ${s}% ${l}% / 0.7);`; 1028 + } 1029 + 912 1030 // "alpine~3.23~x86_64" → "alpine 3.23". Used for tooltips and 913 1031 // dropdowns where the full distro name+version is wanted. 914 1032 function shortDistro(osKey) { ··· 1164 1282 `</summary>`; 1165 1283 const pop = document.createElement("div"); 1166 1284 pop.className = "os-popover"; 1285 + // Snapshot the manifest order once so the "deselect = jump to the 1286 + // next one" behaviour is stable regardless of insertion order. 1287 + const osKeys = [...state.manifests.keys()]; 1167 1288 for (const [key, mm] of state.manifests) { 1168 1289 const f = mm.summary.build_failed + mm.summary.install_failed 1169 1290 + mm.summary.fetch_failed + mm.summary.solve_failed; 1170 1291 const p = parseOsKey(key); 1171 1292 const btn = document.createElement("button"); 1172 - if (key === state.active) btn.className = "current"; 1293 + const isCurrent = key === state.active; 1294 + if (isCurrent) btn.className = "current"; 1295 + btn.title = isCurrent 1296 + ? "click again to switch to the next OS" 1297 + : `switch to ${p.full}`; 1173 1298 btn.innerHTML = 1174 1299 `${escapeHtml(p.full)} <span style="color:var(--fg-faint)">(${mm.n_packages})</span>` + 1175 1300 (f > 0 ? `<span class="fail-count">${f} failed</span>` : ""); 1176 1301 btn.onclick = (e) => { 1177 1302 e.preventDefault(); 1178 - state.active = key; 1303 + let next = key; 1304 + if (isCurrent && osKeys.length > 1) { 1305 + // Toggle: cycle to the next manifest in the list, wrapping 1306 + // around. Gives the popover a real "deselect" affordance — 1307 + // there's always *some* OS active because the page has 1308 + // nothing to render otherwise. 1309 + const idx = osKeys.indexOf(key); 1310 + next = osKeys[(idx + 1) % osKeys.length]; 1311 + } 1312 + state.active = next; 1179 1313 state.expanded = null; 1180 1314 state.highlightUpstream = null; 1181 1315 writeHash(state.active, null, overlaysFromState()); ··· 1379 1513 if (!callers || callers.length === 0) return ""; 1380 1514 const rows = callers.map(c => { 1381 1515 const lbl = c.overlay 1382 - ? `<span class="overlay-tag">@${escapeHtml(c.overlay.handle)}${c.overlay.version ? " " + escapeHtml(c.overlay.version) : ""}</span>` 1516 + ? `<span class="overlay-tag" style="${handleStyle(c.overlay.handle)}">@${escapeHtml(c.overlay.handle)}${c.overlay.version ? " " + escapeHtml(c.overlay.version) : ""}</span>` 1383 1517 : `<span class="overlay-tag none">(no overlay)</span>`; 1384 1518 const tc = c.toolchain ? ` · ${escapeHtml(c.toolchain)}` : ""; 1385 1519 return `<tr> ··· 1442 1576 const c = ev.context || {}; 1443 1577 const o = c.overlay; 1444 1578 return o 1445 - ? `<span class="overlay-tag">@${escapeHtml(o.handle)}${o.version ? " " + escapeHtml(o.version) : ""}</span>` 1579 + ? `<span class="overlay-tag" style="${handleStyle(o.handle)}">@${escapeHtml(o.handle)}${o.version ? " " + escapeHtml(o.version) : ""}</span>` 1446 1580 : `<span class="overlay-tag none">(no overlay)</span>`; 1447 1581 } 1448 1582 ··· 1665 1799 return `<div class="pkg-list">${headerRow}${rows.map(r => { 1666 1800 if (r.__header) { 1667 1801 const lbl = r.__header === "(none)" ? "(no overlay)" : "@" + r.__header; 1668 - return `<div class="group-header">${escapeHtml(lbl)}<span class="group-count">${r.count} pkg(s)</span></div>`; 1802 + const style = handleHeaderStyle(r.__header); 1803 + return `<div class="group-header"${style ? ` style="${style}"` : ""}>${escapeHtml(lbl)}<span class="group-count">${r.count} pkg(s)</span></div>`; 1669 1804 } 1670 1805 const grp = OUTCOME_GROUPS[r.headline_outcome] || "ok"; 1671 1806 const expanded = state.expanded === r.layer_hash ? " expanded" : ""; ··· 1678 1813 const renderHandleChip = (h) => 1679 1814 h === "(none)" 1680 1815 ? `<span class="overlay-tag none">∅</span>` 1681 - : `<span class="overlay-tag">@${escapeHtml(h)}</span>`; 1816 + : `<span class="overlay-tag" style="${handleStyle(h)}">@${escapeHtml(h)}</span>`; 1682 1817 let chips; 1683 1818 if (handles.length === 0) { 1684 1819 chips = `<span class="overlay-tag none">—</span>`; ··· 1693 1828 <div class="handle-dd-list">${list}</div> 1694 1829 </details>`; 1695 1830 } 1696 - const rowHtml = `<div class="pkg-row${expanded}${upstream}" data-hash="${escapeHtml(r.layer_hash)}"> 1831 + const rowStyle = handleRowStyle(handles); 1832 + const rowHtml = `<div class="pkg-row${expanded}${upstream}" data-hash="${escapeHtml(r.layer_hash)}"${rowStyle ? ` style="${rowStyle}"` : ""}> 1697 1833 <span class="outcome-badge outcome-${grp}">${r.headline_outcome.replace(/_/g, " ")}</span> 1698 1834 <span><span class="pkg-name">${escapeHtml(r.pkg.name)}</span> <span class="pkg-version">${escapeHtml(r.pkg.version)}</span></span> 1699 1835 ${renderDistroChips(r.pkg)} ··· 1722 1858 } 1723 1859 } 1724 1860 1861 + // Capture which input/textarea has focus and its current selection so a 1862 + // re-render that blows the DOM away can put the caret back where the 1863 + // user left it. Without this, every keystroke in the filter box (which 1864 + // triggers a full re-render) drops focus after the first character. 1865 + function captureFocus() { 1866 + const el = document.activeElement; 1867 + if (!el || !el.id) return null; 1868 + const tag = el.tagName; 1869 + if (tag !== "INPUT" && tag !== "TEXTAREA") return null; 1870 + return { 1871 + id: el.id, 1872 + start: el.selectionStart, 1873 + end: el.selectionEnd, 1874 + }; 1875 + } 1876 + 1877 + function restoreFocus(snap) { 1878 + if (!snap) return; 1879 + const el = document.getElementById(snap.id); 1880 + if (!el) return; 1881 + el.focus(); 1882 + if (snap.start != null && el.setSelectionRange) { 1883 + try { el.setSelectionRange(snap.start, snap.end); } catch (_) { /* unsupported */ } 1884 + } 1885 + } 1886 + 1725 1887 function render() { 1888 + const focusSnap = captureFocus(); 1726 1889 renderHeader(); 1727 1890 const m = state.manifests.get(state.active); 1728 1891 if (!m) { 1729 1892 document.getElementById("content").innerHTML = `<div class="empty">No manifest found.</div>`; 1893 + restoreFocus(focusSnap); 1730 1894 return; 1731 1895 } 1732 1896 document.getElementById("content").innerHTML = 1733 1897 renderStatusBar(m) + renderFilters(m) + renderPackages(m, state.active); 1898 + restoreFocus(focusSnap); 1734 1899 1735 1900 document.getElementById("filter").oninput = (e) => { state.filter = e.target.value; render(); }; 1736 1901 document.getElementById("sort").onchange = (e) => { state.sortMode = e.target.value; render(); }; ··· 1951 2116 state.audits.set(k, a); 1952 2117 } 1953 2118 } 2119 + invalidateHandleColors(); 1954 2120 if (state.manifests.size === 0) { 1955 2121 document.getElementById("content").innerHTML = 1956 2122 `<div class="empty">No manifests found at any of:<br><br>` +