diff --git a/.github/workflows/black-lint.yml b/.github/workflows/black-lint.yml index 311edbe..21c7f51 100644 --- a/.github/workflows/black-lint.yml +++ b/.github/workflows/black-lint.yml @@ -3,12 +3,8 @@ name: Black Lint on: push: branches: [main, dev] - paths: - - '**/*.py' pull_request: branches: [main] - paths: - - '**/*.py' workflow_dispatch: concurrency: diff --git a/README.md b/README.md index 0701a19..4e1cfce 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ #### [④ Special Image Exporter: html2canvas](https://html2canvas.hertzen.com) -Is the card-export engine since LinesShines v1.3.0~~ +Is the card-export engine since [LinesShines v1.3.0](https://github.com/StarsExpress/LinesShines/releases/tag/v1.3.0)~~ I truly recommend [html2canvas](https://github.com/niklasvh/html2canvas) for: diff --git a/frontend/card-export.js b/frontend/card-export.js index fa52593..d59a09d 100644 --- a/frontend/card-export.js +++ b/frontend/card-export.js @@ -80,20 +80,21 @@ function buildExportClone(cardEl) { const panelEl = clone.querySelector(".scout-card-panel"); if (panelEl) panelEl.style.cssText = "height: auto; max-height: none; overflow: visible;"; - // Merge Card's frozen Player/Team/Linemates columns (BLUEPRINT.md's Excel - // frozen-pane treatment) are position:sticky — turns out that does NOT - // gracefully fall back to their normal flow position once their scroll - // container stops scrolling, at least not for this off-screen fixed- - // position clone: measured directly, they get shoved to sit near the far - // right edge of the widened row instead of staying put at the left, - // because a sticky element still hunts for a scrolling ancestor (falling - // back to the page itself, whose scroll position has nothing to do with - // this off-screen clone) and clamps to whatever position that leaves - // reachable within its own cell. Forcing position:static (clearing the - // sticky left offset with it) sidesteps that entirely — with nothing - // scrolling underneath them in the export, sticky was only ever pinning - // them to their own already-correct flow position anyway. - clone.querySelectorAll(".merge-table-frozen").forEach((cell) => { + // Merge Card's frozen Player/Team/Linemates columns and Linemate Card's + // frozen Player column (both BLUEPRINT.md's Excel frozen-pane treatment) + // are position:sticky — turns out that does NOT gracefully fall back to + // their normal flow position once their scroll container stops scrolling, + // at least not for this off-screen fixed-position clone: measured + // directly, they get shoved to sit near the far right edge of the widened + // row instead of staying put at the left, because a sticky element still + // hunts for a scrolling ancestor (falling back to the page itself, whose + // scroll position has nothing to do with this off-screen clone) and + // clamps to whatever position that leaves reachable within its own cell. + // Forcing position:static (clearing the sticky left offset with it) + // sidesteps that entirely — with nothing scrolling underneath them in the + // export, sticky was only ever pinning them to their own already-correct + // flow position anyway. + clone.querySelectorAll(".merge-table-frozen, .linemate-table-frozen").forEach((cell) => { cell.style.position = "static"; cell.style.left = ""; }); @@ -101,7 +102,7 @@ function buildExportClone(cardEl) { // Lays every column flat in one row instead of clipping to whatever was // scrolled into view, then widens the card itself to fit. let requiredWidth = 0; - clone.querySelectorAll(".merge-table-wrap, .linemate-summary-wrap").forEach((scrollEl) => { + clone.querySelectorAll(".merge-table-wrap, .linemate-table-wrap, .linemate-summary-wrap").forEach((scrollEl) => { scrollEl.style.overflow = "visible"; requiredWidth = Math.max(requiredWidth, scrollEl.scrollWidth); }); diff --git a/frontend/dom.js b/frontend/dom.js index cb5f614..818597a 100644 --- a/frontend/dom.js +++ b/frontend/dom.js @@ -26,6 +26,7 @@ export const els = { teamsSelectAll: document.getElementById("teams-select-all"), teamsSelectNone: document.getElementById("teams-select-none"), playersControl: document.getElementById("players-control"), + playersInfoSlot: document.getElementById("players-info-slot"), playersResetBtn: document.getElementById("players-reset-btn"), playersBtn: document.getElementById("players-toggle-btn"), playersSummary: document.getElementById("players-select-summary"), @@ -58,6 +59,7 @@ export const els = { pcsInspectBtn: document.getElementById("pcs-inspect-btn"), pcsPanel: document.getElementById("pcs-panel"), pcsSinglesList: document.getElementById("pcs-singles-list"), + pcsSinglesEmpty: document.getElementById("pcs-singles-empty"), pcsMergedList: document.getElementById("pcs-merged-list"), pcsMergedEmpty: document.getElementById("pcs-merged-empty"), // Single Cards add-search (v1.2.0 §4) — Pinned Players' only entry point. diff --git a/frontend/filters.js b/frontend/filters.js index 314de38..3ae2fe3 100644 --- a/frontend/filters.js +++ b/frontend/filters.js @@ -82,6 +82,21 @@ export function selectedTeamCodes() { return Array.from(els.teamsChecklist.querySelectorAll("input[type=checkbox]:checked")).map((cb) => cb.value); } +// Teams and Players combine via union (CLAUDE.md's "Teams and Players +// filters combine via union" section) — a full 32-team selection highlights +// every player regardless of Players, which makes picking a specific player +// on top of it a no-op the user almost certainly didn't intend. Auto-resets +// Teams to none selected in exactly that one case. Deliberately scoped to +// the all-32 case only: a partial selection (even 5 or 31 teams) is a +// legitimate combination the user chose on purpose and must survive a +// player pick untouched — only ever called from the player-pick handler +// below, never from anywhere a partial selection should be left alone. +function resetTeamsIfAllSelected() { + if (selectedTeamCodes().length !== allTeamCodes().length) return; + els.teamsChecklist.querySelectorAll("input[type=checkbox]").forEach((cb) => (cb.checked = false)); + updateTeamsSummary(); +} + export function teamOptionRow(code) { const label = document.createElement("label"); label.className = "team-option"; @@ -257,6 +272,7 @@ export function renderPlayersDropdown(matches) { opt.append(name, team); opt.addEventListener("click", () => { selectedPlayers.set(record.player, record); + resetTeamsIfAllSelected(); renderPlayerChips(); updatePendingState(); els.playersInput.focus(); @@ -328,7 +344,16 @@ export function populateCategoryDependentControls() { els.position.appendChild(opt); }); - // Seasons (already sorted desc by the API) + // Seasons (already sorted desc by the API) — rebuilding the 's own .value is always a string — String(s) here is what makes + // the .includes() comparison below actually match. + const previousSeason = els.season.value; els.season.innerHTML = ""; cat.seasons.forEach((s) => { const opt = document.createElement("option"); @@ -336,6 +361,7 @@ export function populateCategoryDependentControls() { opt.textContent = s; els.season.appendChild(opt); }); + if (cat.seasons.map(String).includes(previousSeason)) els.season.value = previousSeason; // Metrics const metricKeys = Object.keys(cat.metrics); @@ -423,6 +449,33 @@ export function updateThresholdRange() { els.thresholdNumber.value = els.threshold.value; } +// Fired immediately on a Category switch (main.js's attachEvents()), unlike +// the season/position case in updateThresholdRange() above which only +// clamps the user's existing value — the two threshold_field scales (PR Opp +// vs Non Spike PB Snaps) aren't comparable, so leaving the outgoing +// category's number on screen until Apply would be actively misleading. +// Always snaps to the new category's configured default and never tries to +// preserve whatever the user had set for the outgoing category. The +// accurate slider max (which needs the new category's fetched data) still +// gets recomputed at Apply time via loadCurrentSlice()/updateThresholdRange() +// — this only fixes what's on screen immediately. Sets both the slider and +// the number input together so they can never fall out of sync with each +// other, same as every other place both controls change at once. +export function resetThresholdToCategoryDefault() { + const cat = currentCategoryMeta(); + const defaultValue = cat.default_threshold ?? 0; + // The outgoing category's slider max may be smaller than the incoming + // default (e.g. a narrow DL pool's max sitting below OL's 300 default) — + // extend it so the browser doesn't silently clamp the value we're about + // to set. loadCurrentSlice() overwrites this with the real max at Apply. + if (Number(els.threshold.max) < defaultValue) { + els.threshold.max = defaultValue; + els.thresholdNumber.max = defaultValue; + } + els.threshold.value = defaultValue; + els.thresholdNumber.value = defaultValue; +} + export function closeFiltersDrawer() { els.filtersDrawer.classList.remove("open"); els.filtersToggle.setAttribute("aria-expanded", "false"); diff --git a/frontend/index.html b/frontend/index.html index 7c00835..266530d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,7 +8,7 @@ - + @@ -38,7 +38,7 @@

LinesShines · 鋒光

"No Line No Shine."

Infrastructure is the one that nearly always starts everything, but quite often gets ignored thanks to its unadorned hard works and grit & grind styles.

- For all these under-appreciated. + For all these under-appreciated. Let's explore 🗺️

@@ -99,10 +99,7 @@

LinesShines · 鋒光

control-bar sizing gotcha in CLAUDE.md) every time a player is added. -->
- i - +
@@ -233,7 +230,7 @@

LinesShines · 鋒光

-

Single Cards

+

Player Profiles

-

Type a player's name to add a card.

+

Type a player's name to add a card.

@@ -260,7 +257,7 @@

Single Cards

-

Merged Cards

+

Player Comparisons

@@ -327,7 +324,10 @@

Merged Cards

Linemates
-

+
    +
  • +
  • +
@@ -355,8 +355,10 @@

Merged Cards

-

-

Please scroll right for more metrics.

+
@@ -366,15 +368,17 @@

Merged Cards

- +