Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
76716d8
Added players spotlight.
StarsExpress Jul 21, 2026
c48245d
Added CI to dev branch.
StarsExpress Jul 21, 2026
158f64d
Adjusted workflows triggers.
StarsExpress Jul 21, 2026
9ecd3f9
Allowed multi cards and multi player selection.
StarsExpress Jul 25, 2026
9cc38e8
Allowed players cards to be expandable and shrinkable.
StarsExpress Jul 26, 2026
c9c2461
Enhanced layout.
StarsExpress Jul 30, 2026
bab631a
Merge branch 'main' into dev
StarsExpress Jul 30, 2026
5dfbba4
Removed highlightSubtitle off app.js.
StarsExpress Jul 30, 2026
bb68363
Added watermark for downloading PNGs.
StarsExpress Jul 31, 2026
565ef1f
Added player cards merger framework.
StarsExpress Jul 31, 2026
08ba9fa
Refined linemates card and fixed bugs.
StarsExpress Aug 1, 2026
657a141
Refined cards merger.
StarsExpress Aug 2, 2026
19c87e4
Merge branch 'main' into dev
StarsExpress Aug 2, 2026
aefced7
Completed modularization.
StarsExpress Aug 2, 2026
181202e
Adjusted comment for config.py.
StarsExpress Aug 2, 2026
9b534c7
Added freeze panes for merged cards.
StarsExpress Aug 3, 2026
9191339
Enhanced UI.
StarsExpress Aug 4, 2026
1f76bc6
Reformatted code.
StarsExpress Aug 4, 2026
9113cde
Added save card buttons.
StarsExpress Aug 5, 2026
3e804c3
Adjusted README.md.
StarsExpress Aug 5, 2026
d5e0179
Merge branch 'main' into dev
StarsExpress Aug 5, 2026
6436e98
Aligned single cards & merged cards behaviors.
StarsExpress Aug 7, 2026
782fb10
Changed majority of fonts to Oswald.
StarsExpress Aug 7, 2026
ffba1cf
Widened merged cards default width.
StarsExpress Aug 8, 2026
9aacba6
Enhanced UI.
StarsExpress Aug 8, 2026
e6ba26e
Removed paths conditions off black-lint.yml.
StarsExpress Aug 8, 2026
6eeb53e
Enhanced UI.
StarsExpress Aug 8, 2026
c506e76
Added sorting direction indicators for data frames.
StarsExpress Aug 8, 2026
26e3a04
Added BG & text color for percentiles.
StarsExpress Aug 8, 2026
9bcd86b
Added more Neubrutalism.
StarsExpress Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/black-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ name: Black Lint
on:
push:
branches: [main, dev]
paths:
- '**/*.py'
pull_request:
branches: [main]
paths:
- '**/*.py'
workflow_dispatch:

concurrency:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
31 changes: 16 additions & 15 deletions frontend/card-export.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,28 +80,29 @@ 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 = "";
});

// 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);
});
Expand Down
2 changes: 2 additions & 0 deletions frontend/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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.
Expand Down
55 changes: 54 additions & 1 deletion frontend/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -328,14 +344,24 @@ export function populateCategoryDependentControls() {
els.position.appendChild(opt);
});

// Seasons (already sorted desc by the API)
// Seasons (already sorted desc by the API) — rebuilding the <select>'s
// options resets its value to whichever option lands first (the newest
// season) unless we explicitly restore the previous selection, so a
// category switch doesn't silently drag Season back to default along with
// it. Only restored when the prior season still exists for the new
// category; otherwise the browser's own first-option default stands.
// cat.seasons holds numbers (straight from /api/metadata's JSON), but a
// <select>'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");
opt.value = s;
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);
Expand Down Expand Up @@ -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");
Expand Down
60 changes: 34 additions & 26 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Oswald:wght@450;600&display=swap" rel="stylesheet">

<link rel="stylesheet" href="style.css?v={{VERSION}}" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
Expand Down Expand Up @@ -38,7 +38,7 @@ <h1 class="hero-title">LinesShines · 鋒光</h1>
"No Line No Shine."<br><br>
Infrastructure is the one that nearly always starts everything,
but quite often gets ignored thanks to its unadorned hard works and grit & grind styles.<br><br>
For all these under-appreciated.
For all these under-appreciated. Let's explore 🗺️
</p>
</div>
</div>
Expand Down Expand Up @@ -99,10 +99,7 @@ <h1 class="hero-title">LinesShines · 鋒光</h1>
control-bar sizing gotcha in CLAUDE.md) every time a player is added. -->
<div class="control control-players" id="players-control">
<div class="players-label-row">
<span class="info-hint" tabindex="0"
title="Teams and Players combine as a union: a player is highlighted if either his team is chosen, or his name is selected."
data-tooltip="Teams and Players combine as a union: a player is highlighted if either his team is chosen, or his name is selected.">i</span>
<label id="players-label" for="players-toggle-btn">Players</label>
<span class="info-popover-slot" id="players-info-slot"></span>
<button type="button" id="players-reset-btn" class="players-reset-btn">Clear</button>
</div>
<div class="players-select">
Expand Down Expand Up @@ -233,7 +230,7 @@ <h1 class="hero-title">LinesShines · 鋒光</h1>
<div class="pcs-columns">
<div class="pcs-column">
<div class="pcs-column-header">
<h3 class="pcs-column-title">Single Cards</h3>
<h3 class="pcs-column-title">Player Profiles</h3>
<!-- Single Cards' only entry point (BLUEPRINT_PinnedPlayers.md
v1.2.0 §4) — a plot click no longer adds here, it only opens
the floating card (see viewFloatingCard() in app.js). Same
Expand All @@ -252,15 +249,15 @@ <h3 class="pcs-column-title">Single Cards</h3>
line stands in for it, in the same row position as the Merged
Cards column's own hint ("Click Create...", #pcs-merged-empty)
below its header. -->
<p class="pcs-empty">Type a player's name to add a card.</p>
<p class="pcs-empty" id="pcs-singles-empty">Type a player's name to add a card.</p>
<div class="pcs-list" id="pcs-singles-list"></div>
</div>

<div class="pcs-columns-divider" aria-hidden="true"></div>

<div class="pcs-column">
<div class="pcs-column-header">
<h3 class="pcs-column-title">Merged Cards</h3>
<h3 class="pcs-column-title">Player Comparisons</h3>
<button type="button" class="merge-btn" id="pcs-create-btn">Create</button>
</div>
<div class="pcs-list" id="pcs-merged-list"></div>
Expand Down Expand Up @@ -327,7 +324,10 @@ <h3 class="pcs-column-title">Merged Cards</h3>
<i class="fa-solid fa-people-group"></i> Linemates
</button>
</div>
<p class="scout-card-pool"></p>
<ul class="scout-card-note-list">
<li class="scout-card-note scout-card-pool"></li>
<li class="scout-card-note scout-card-threshold"></li>
</ul>
<dl class="scout-stats"></dl>
</div>
</div>
Expand Down Expand Up @@ -355,8 +355,10 @@ <h3 class="pcs-column-title">Merged Cards</h3>
</div>
</div>
<div class="merge-card-notes">
<p class="merge-card-pool"></p>
<p class="merge-card-scroll-hint">Please scroll right for more metrics.</p>
<ul class="merge-card-note-list">
<li class="merge-card-note merge-card-pool"></li>
<li class="merge-card-note merge-card-scroll-hint">Please scroll right for more metrics.</li>
</ul>
</div>
<div class="merge-table-wrap">
<table class="merge-table"></table>
Expand All @@ -366,15 +368,17 @@ <h3 class="pcs-column-title">Merged Cards</h3>
</aside>
</template>

<!-- Roster (percentiles only, own-position pool) + the 3M/RSWA summary
table (BLUEPRINT.md §2) — filled in by openLinemateCard() in app.js.
Header row now mirrors the Player Card's (.scout-logo-wrap +
.scout-id/.scout-name/.scout-meta) rather than a bare title — see
openLinemateCard()/renderLinemateCardBody(). No linemate toggle
anywhere on this card: the recursion guard from §2.3 is enforced by
simply never rendering one here — the second header row
(.scout-card-actions) that holds a Linemates toggle on a Player Card
holds the "All numbers..."/"Tooltip each..." disclaimer here instead. -->
<!-- Roster table (percentiles only, own-position pool, DataFrame-style
columns matching Merge Card's — see makeTeamCell()/renderMergeCardBody()
in merge-card.js) + the 3M/RSWA summary table (BLUEPRINT.md §2) —
filled in by openLinemateCard() in app.js. Header row mirrors the
Player Card's (.scout-logo-wrap + .scout-id/.scout-name/.scout-meta)
rather than a bare title — see openLinemateCard()/renderLinemateCardBody().
No linemate toggle anywhere on this card, roster table included: the
recursion guard from §2.3 is enforced by simply never rendering one
here — the second header row (.scout-card-actions) that holds a
Linemates toggle on a Player Card holds the "All numbers..."
disclaimer here instead. -->
<template id="linemate-card-template">
<aside class="scout-card scout-card--linemate">
<button type="button" class="card-save-btn" aria-label="Save card as PNG">
Expand All @@ -398,10 +402,14 @@ <h3 class="pcs-column-title">Merged Cards</h3>
</div>
</div>
<div class="scout-card-actions linemate-card-notes">
<p class="linemate-card-note">All numbers are a player's percentiles among his position.</p>
<p class="linemate-card-hint">Tooltip each percentile for more info.</p>
<ul class="linemate-card-note-list">
<li class="linemate-card-note linemate-card-roster-note"></li>
<li class="linemate-card-note">All numbers are a player's percentiles among his position.</li>
</ul>
</div>
<div class="linemate-table-wrap">
<table class="linemate-table"></table>
</div>
<div class="linemate-roster"></div>
<button type="button" class="linemate-see-more" hidden>See more</button>
<div class="linemate-summary-wrap">
<h4 class="linemate-summary-title">Line Summary</h4>
Expand Down Expand Up @@ -436,7 +444,7 @@ <h4 class="linemate-summary-title">Line Summary</h4>
<div class="merge-confirm-box" role="alertdialog" aria-modal="true" aria-labelledby="merge-confirm-title">
<p class="merge-confirm-title" id="merge-confirm-title">Clear your Pinned Players?</p>
<p class="merge-confirm-body">
This filter change recomputes the player pool — every Single Card and Merged Card in your
This filter change recomputes the player pool — every Player Profile and Player Comparison in your
Pinned Players, built against the old pool, will be dissolved (open Linemate Cards will
refresh or close as needed). This can't be undone.
</p>
Expand All @@ -455,7 +463,7 @@ <h4 class="linemate-summary-title">Line Summary</h4>
discards it untouched, no card created. -->
<div class="merge-edit-overlay" id="merge-create-overlay" hidden>
<div class="merge-edit-box" role="dialog" aria-modal="true" aria-labelledby="merge-create-title">
<p class="merge-edit-title" id="merge-create-title">Create Merged Card</p>
<p class="merge-edit-title" id="merge-create-title">Create Player Comparison</p>

<div class="merge-edit-members" id="merge-create-members"></div>

Expand Down
Loading
Loading