Skip to content

Commit 0ff0e16

Browse files
committed
v1.8.12 — Storage view: kill the per-keystroke and per-hover flash on search-active racks
1 parent 2af3a81 commit 0ff0e16

5 files changed

Lines changed: 61 additions & 30 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ Versions follow [Semantic Versioning](https://semver.org/).
55

66
---
77

8+
## v1.8.12 — 2026-05-31
9+
10+
### Fixed
11+
12+
- **Storage view no longer flashes on every search keystroke or rack hover.** Typing in the search bar with the Storage view open used to flash the whole grid at every letter, and sweeping the mouse between racks that both contained search matches produced a visible flash too — most noticeably on large inventories like a friend's read-only view. Both issues are now gone. The root causes were CSS animations on properties that force a per-frame GPU repaint (the orange "match" ring pulse), a hover-triggered reflow that decaled every match-slot by 14 px (the column-number coords row used to expand from 0 to 14 px on rack hover), and a full DOM rebuild of all rack slots on every keystroke. Side effect: each rack reserves a small space above the first shelf for the column-number coords in permanence (they still only become visible when you hover the rack), so racks are ever-so-slightly taller than before.
13+
14+
---
15+
816
## v1.8.11 — 2026-05-31
917

1018
### Added

‎WORKLOG.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
# Worklog — v1.8.11 (in progress)
1+
# Worklog — v1.8.12 (in progress)
22

33
## Added
44

55
## Changed
6-
- Storage view: the in-stats "+ New Rack" tile is gone — the header **+ Add Rack** button (same one that becomes "Add Product" in inventory views and "Add Device" in printer views) is now the primary "add a rack" entry point. The header button label and click handler dispatch by current view mode (inventory / printer / storage). The empty-state CTA when the user has zero racks still works — `renderer/inventory.js` (`setViewMode`, `btnAddProduct` handler, `renderRackView`), `renderer/locales/*.json` (new `addRackBtn` key)
7-
- Storage view "not stored" side panel: contextual **+ Add Rack** CTA with a short hint, shown only when the active unranked spool count exceeds the number of free slots (i.e. the user genuinely needs more rack capacity). Orange accent to echo the "not stored" pill in the stats bar — `renderer/inventory.js` (`renderRackView`, click handler), `renderer/css/30-racks.css` (`.rp-side-add-rack*`), `renderer/locales/*.json` (new `rackNoSpaceHint` key)
86

97
## Fixed
10-
- Storage view: rack stats no longer count "ghost" spools — both the global header (`filled / total slots`, `free`) and the per-rack header (`filled/total`) used to include any spool that still had a `rackId` set, even when the rack had been deleted or the spool's `rackLevel`/`rackPos` were out of bounds. That allowed the filled count to exceed total capacity (e.g. `130/117 slots`, `0 free`) and per-rack numbers to be larger than what was actually rendered. A shared `_isInValidSlot()` helper now requires the rackId to match a current rack and the level/position to fall inside its grid, so stats match what's visible in the slots — `renderer/inventory.js` (`renderRackView`)
11-
- Deleting a rack now fully unassigns the spools that were inside it (both modern `rack: null` and the legacy flat `rack_id` / `level` / `position` fields are cleared via `FV.delete()`). The old code only nulled the modern field, leaving the legacy ones intact — `normalizeRow` then resolved `rackId` via the flat fallback and the spool stayed ghost-assigned to the deleted rack forever. Pre-existing orphan references are also auto-cleaned on first opening the Storage view (one-shot batch, guarded against concurrent fires) — `renderer/inventory.js` (`deleteRack`, `_cleanupOrphanRackRefs`)
12-
- Storage view "not stored" panel now also surfaces spools whose `rackId` points to a rack that no longer exists (orphans). Until the background cleanup nulls their stale rackId, they're shown as unranked so the user can immediately see and re-assign them — `renderer/inventory.js` (`getUnrackedSpools`)
8+
- Storage view: stop the GPU `tile_manager.cc:997 WARNING: tile memory limits exceeded` flood and the visual flash that happened on every search keystroke and when hover-sweeping between racks containing search matches. Five coupled changes:
9+
- `renderer/css/30-racks.css` — the search-match pulse on `.rp-slot--match` no longer animates `box-shadow` (per-frame GPU repaint that saturated tile memory once ~15+ matches were on screen). It now animates `transform: scale` with `will-change: transform`, keeping the shadow static at the midpoint of the previous keyframe so visibility is preserved.
10+
- `renderer/css/30-racks.css` — `.rp-row--header` (the column-number row) now reserves its 14px height permanently instead of expanding from 0 → 14px on rack-hover. The hover-driven height change forced a reflow that re-rasterized every match-slot's compositor layer, producing a visible flash between racks. Coords are now revealed via opacity-only on `.rp-col-label`.
11+
- `renderer/css/30-racks.css` — on hover, `.rp-slot--match` cancels the pulse and snaps back to `scale(1)`. The parent `.rp-slot--filled:hover` rule applies `transform: translateY(-1px)`, which overrode the scale animation and produced a visible flash whenever the mouse swept across multiple animating match-slots in quick succession.
12+
- `renderer/css/30-racks.css` — `.rp-slot--filled.rp-dim` no longer transitions `filter`. Animating `filter: grayscale` on 100-300 slots whenever the dim set churned (every search keystroke, every filter change) forced a per-frame repaint of the whole grid. Opacity transition stays (compositor-friendly).
13+
- `renderer/inventory.js` — search/clear listeners (`#searchInv`) no longer call the full `renderInventory()` → `renderRackView()` rebuild in rack view. They now short-circuit to `applyRackSearchDim()` which just toggles `rp-dim`/`rp-slot--match` classes on existing slots. Stats (racks / filled / locked) don't depend on the search, so the class toggle is sufficient.
1314

1415
## Removed
1516

1617
## i18n
17-
- Added: `addRackBtn`, `rackNoSpaceHint`, `camWallEmptyTitle`, `camWallEmptySub` — 9 locales. The two cam-wall keys were referenced in code but missing from all locales, so the cam view's empty state was showing the raw key names ("camWallEmptyTitle" / "camWallEmptySub") instead of localized text.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tigertag-inventory",
3-
"version": "1.8.11",
3+
"version": "1.8.12",
44
"description": "Tiger Studio Manager — desktop app",
55
"main": "main.js",
66
"engines": {

‎renderer/css/30-racks.css‎

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -580,16 +580,19 @@
580580
text-transform: uppercase;
581581
transition: opacity .18s;
582582
}
583-
/* Column number header — sits above the first shelf, no border, no padding */
583+
/* Column number header — sits above the first shelf, no border, no padding.
584+
Height is reserved permanently (was height: 0 / 14px on hover): the height
585+
transition forced a reflow of every slot on rack-hover, which re-rasterized
586+
the compositor layers of `.rp-slot--match` slots (transform: scale animation)
587+
and produced a visible flash when sweeping between racks that both contained
588+
matches. Reveal is now opacity-only on `.rp-col-label` (compositor-only). */
584589
.rp-row--header {
585590
padding-bottom: 4px;
586591
border-bottom: none;
587-
/* Collapse the row when coordinates are hidden so the rack is more compact */
588-
height: 0;
592+
height: 14px;
589593
overflow: hidden;
590594
padding-top: 0;
591595
margin-bottom: 0;
592-
transition: height .18s, padding-top .18s;
593596
}
594597
.rp-row--header .rp-col-label {
595598
font-size: 10px; font-weight: 700; color: var(--muted);
@@ -604,11 +607,6 @@
604607
body.is-dragging-spool .rp-row-label {
605608
opacity: .65;
606609
}
607-
.rp-rack:hover .rp-row--header,
608-
body.is-dragging-spool .rp-row--header {
609-
height: 14px;
610-
padding-top: 0;
611-
}
612610
.rp-rack:hover .rp-row--header .rp-col-label,
613611
body.is-dragging-spool .rp-row--header .rp-col-label {
614612
opacity: .55;
@@ -746,35 +744,51 @@
746744
/* Search dim — applied to filled rack slots that don't match the query.
747745
The slot stays in place but loses color saturation and most opacity, so
748746
the matching slots visually pop. */
747+
/* No `transition: filter` — animating filter:grayscale per-frame on 100-300
748+
slots when the search churns the dim set on every keystroke flashed the
749+
entire grid. opacity is compositor-friendly so its transition stays. */
749750
.rp-slot--filled.rp-dim {
750751
opacity: .18;
751752
filter: grayscale(.85);
752-
transition: opacity .15s, filter .15s;
753+
transition: opacity .15s;
753754
}
754755
.rp-slot--filled.rp-dim:hover { opacity: .4; } /* hint that it's still there */
755756

756757
/* Search-match indicator on the slot CONTAINER — makes the slot stand
757758
out via a border+glow on the box itself, NOT via the inner color fill.
758759
This is critical for depleted spools (.rp-fill height ~ 0): the slot's
759760
box still glows in orange so the user sees it when they search. */
761+
/* Static shadow uses the midpoint of the old keyframe so visibility is
762+
preserved. Pulse is driven by `transform: scale` (compositor-only) instead
763+
of animating box-shadow — animating shadow forced a GPU repaint on every
764+
frame, saturating Chromium's tile memory (`tile_manager.cc:997 WARNING:
765+
tile memory limits exceeded`) when ~15+ matches were on screen. */
760766
.rp-slot--filled.rp-slot--match {
761767
border-color: var(--primary) !important;
762768
box-shadow:
763-
0 0 0 2px rgba(249,115,22,.55),
764-
0 0 8px rgba(249,115,22,.55),
769+
0 0 0 2.5px rgba(249,115,22,.7),
770+
0 0 11px rgba(249,115,22,.7),
765771
inset 0 0 0 1px rgba(255,255,255,.28);
766772
z-index: 2;
767773
animation: rp-slot-match-pulse 1.6s ease-in-out infinite;
774+
/* Pre-promote to a compositor layer so the rack-hover reveal (which expands
775+
`.rp-row--header` from 0 → 14px and shifts every slot below it) doesn't
776+
re-rasterize the scaled match-slots — that re-rasterization was visible
777+
as a brief flash and also re-triggered `tile_manager` warnings. */
778+
will-change: transform;
768779
}
769780
@keyframes rp-slot-match-pulse {
770-
0%, 100% { box-shadow:
771-
0 0 0 2px rgba(249,115,22,.55),
772-
0 0 8px rgba(249,115,22,.55),
773-
inset 0 0 0 1px rgba(255,255,255,.28); }
774-
50% { box-shadow:
775-
0 0 0 3px rgba(249,115,22,.85),
776-
0 0 14px rgba(249,115,22,.85),
777-
inset 0 0 0 1px rgba(255,255,255,.28); }
781+
0%, 100% { transform: scale(1); }
782+
50% { transform: scale(1.06); }
783+
}
784+
/* Hover on a match slot: pause the pulse and snap back to scale(1) — the
785+
parent `.rp-slot--filled:hover` rule applies `transform: translateY(-1px)`,
786+
which would override the animation's `transform: scale` and cause a visible
787+
flash whenever the mouse swept across multiple animating match-slots. The
788+
permanent orange ring already provides ample hover affordance here. */
789+
.rp-slot--filled.rp-slot--match:hover {
790+
animation: none;
791+
transform: scale(1);
778792
}
779793

780794
/* Locked slot — solid amber tint + small lock badge top-right.

‎renderer/inventory.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4995,11 +4995,20 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
49954995
});
49964996
}
49974997

4998+
// In rack view we skip the full DOM rebuild and just re-classify existing
4999+
// slots via applyRackSearchDim() — rebuilding all 100-300 rack slots on each
5000+
// keystroke flashed visually and forced a paint of the entire grid.
5001+
// Stats (racks / filled / locked) don't depend on the search, so a class
5002+
// toggle is sufficient.
5003+
function _onFilterChange() {
5004+
if (state.viewMode === "rack") { applyRackSearchDim(); return; }
5005+
renderInventory();
5006+
}
49985007
$("searchInv").addEventListener("input", e => {
49995008
const v = e.target.value;
50005009
state.search = v.trim();
50015010
_refreshSearchClearVisibility(v);
5002-
renderInventory();
5011+
_onFilterChange();
50035012
});
50045013
// Clear button — wipes the input, refocuses for further typing, and
50055014
// re-renders the inventory immediately. We dispatch an `input` event
@@ -5011,7 +5020,7 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
50115020
inp.value = "";
50125021
state.search = "";
50135022
_refreshSearchClearVisibility("");
5014-
renderInventory();
5023+
_onFilterChange();
50155024
inp.focus();
50165025
});
50175026
// Initial sync — covers the case where the input was pre-populated

0 commit comments

Comments
 (0)