The update banner that shows during an auto-update download was
hosting three inline-styled divs in a row for its progress
indicator:
<div style="display: none; flex: 1; margin: 0 15px;">
<div style="background: rgba(0,0,0,0.3); border-radius: 4px;
height: 8px; overflow: hidden;">
<div id="updateProgressBar"
style="background: white; height: 100%; width: 0%;
transition: width 0.3s;">
Renderer-updates.ts kept reaching in to mutate
updateProgressBar.style.width as the download advanced — works,
but the bar's static look-and-feel (rounded, dark track, 8px tall,
white fill, 0.3s transition) was buried in HTML attributes
instead of CSS.
Extracted to:
- .update-banner-progress-wrap (the flex:1 container with side
margin)
- .update-banner-progress-track (the rounded dark track)
- .update-banner-progress-bar (the white fill, 0% default, transition)
The JS continues to drive width via style.width, which now layers
on top of the class's transition for the smooth animation. Zero
visual change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two separate places (Settings filename templates + clip-cutter
modal custom template) had their own lint state. Each set the
colour by JS as `lintNode.style.color = "#8bc34a"` (green for OK)
or `"#ff8a80"` (red for unknown placeholder). Same intent, different
implementations, different shades than the rest of the app
(--success #00c853 + --error #ff4444).
Extracted to a shared .template-lint class with .ok / .warn modifiers
driven by the canonical CSS vars. The renderers now swap classNames
instead of inline colours.
Also picked up the stale `color: #888` on filenameTemplateHint and
replaced with the existing .form-note utility class (which uses
var(--text-secondary)).
The old .clip-template-lint rule stays as a no-op alias for safety,
but its hard-coded #8bc34a is removed — colour now comes from
.template-lint.ok / .warn. Three hard-coded hex literals retired,
two state branches consolidated, semantics now track the global
palette.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Hide downloaded" checkbox in the VOD filter row was carrying
the inline style:
display:flex; align-items:center; gap:6px;
color: var(--text-secondary); font-size:12px;
cursor:pointer; user-select:none
This is a distinct visual variant of the existing .toggle-row class
(filter-row context, smaller gap, smaller font, secondary text
color) so a separate .inline-toggle class is the cleaner fit
rather than overloading .toggle-row with another modifier.
One site cleaned up now, the class is reusable for future
tool-row inline toggles without copy-pasting the seven inline
properties.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported via screenshot against 4.6.44. The Twitch-style "32h37m9s"
duration pill introduced in 4.6.20 was anchored at bottom: 8px right:
8px inside .vod-card. But .vod-card is the WHOLE card — thumbnail
plus info + action buttons row below — so the badge sat at the
bottom-right of that whole stack, landing on top of the rightmost
action button ("+ Queue") and obscuring it. The user couldnt click
through the badge.
Fix wraps the .vod-thumbnail in a .vod-thumb-wrap div with
position:relative so the badge's absolute positioning anchors to
the thumbnail's bounding box. The badge now sits at the bottom-
right of the actual image, exactly where the Twitch convention
puts it.
.vod-thumb-wrap also gets line-height: 0 to kill the inline
baseline whitespace that would otherwise show as a thin gap
between the image and the .vod-info section below.
The storyboard hover preview overlay (renderer-vod-hover.ts) is
unaffected — it still appendChild's into .vod-card and uses
aspect-ratio: 16/9 to size itself, which matches the thumbnail
zone above the .vod-thumb-wrap container exactly. Verified end-to-
end pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Settings tab has 17 checkbox toggles, each wrapping the input +
its label in a `<label style="display:flex; align-items:center;
gap:8px; margin-top: 8px;">`. The first toggle in a group skipped
the margin-top; one indented sub-toggle added margin-left: 22px.
All 18 sites were carrying the same ~50 chars of inline style.
Extracted to .toggle-row + the adjacent-sibling combinator
.toggle-row + .toggle-row { margin-top: 8px }, which automatically
adds the gap between consecutive toggles without needing the
per-instance override. The one indented case becomes
.toggle-row.indented (single rule, single margin-left).
Replacements done via Edit with replace_all (13 + 2 + 1 = 16
exact-match replacements + the one manual indented variant). Zero
visual change.
This will pay off on the next Settings card that adds a toggle —
class="toggle-row" is six characters of meaning vs the previous
~50 character inline-style copy-paste.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sidebar queue empty state was built via inline-style HTML
template: `<div style="color: var(--text-secondary); font-size:
12px; text-align: center; padding: 15px;">${UI_TEXT.queue.empty}
</div>`. Worked but had two issues:
1. Flat plain-text styling that did not match the
.streamer-list-empty card-style empty hint sitting directly
above it in the same sidebar — visually inconsistent.
2. innerHTML interpolation of a locale string. The string is
safe (locale-controlled, not user input), but the lint hook
pattern-matches innerHTML use anyway, leaving the file flagged
on every audit pass.
Rebuilt via createElement / textContent so no innerHTML touches
the locale string, and extracted the styling into a .queue-empty
CSS class that mirrors the .streamer-list-empty card look
(dashed border + tinted bg + 6px radius + centred text). The two
sidebar empty states now read as a family instead of two
unrelated takes on "empty".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit turned up two ~6-occurrence inline-style patterns in Settings:
- `style="font-size:12px; color:var(--text-secondary);"` on small
sub-labels above stacked inputs (autoCleanupDaysLabel,
TargetLabel, ActionLabel, autoVodPollMinutesLabel,
autoVodMaxAgeHoursLabel, statsLastScannedLabel)
- `style="display:flex; flex-direction:column; gap:4px; flex:1;
min-width:NNNpx;"` on labels that wrap a sublabel + control as a
vertical pair in a flex row (3 of these in the auto-cleanup
grid)
Both lifted to .form-sublabel and .form-stack utility classes.
Plus a .form-note for block-level secondary-coloured text (the
cleanupReport "X files would be freed" panel). The min-width
values stay inline since they vary per call site (120 / 160 / 200).
Zero visual change — the class values match what was inline.
Future edits to "what does a small label look like" go through
one selector instead of grep + sed across six sites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
renderStorageStats was building the storage-stats table in
Einstellungen by setting ~10 style props per element with
.style.padding / .style.color / .style.borderBottom / etc. Verbose
in TypeScript and harder to retheme than CSS-class-based markup.
Extracted to .storage-stats-table family in styles.css:
- Header cells become uppercase 10px tracking-wide labels (matches
the look of the .vod-bulk-count "X selected" label and the
archive search type-pill chips)
- Body rows pick up a hover-background tint for scannability
- Numbers use font-variant-numeric: tabular-nums so file counts
and byte sizes don't jitter as values change between scans
- Last row drops the bottom border so the table doesn't end on
a dangling line
Open-folder button was using .btn-secondary with inline font-size
+ padding overrides — swapped to the .btn-pill class, which is
already the small/compact action button used in vod-bulk-bar and
the archive search results. Visual consistency across the app.
The "Other folders" subheading (.storage-stats-section) gets the
same uppercase-tracking look as the table headers — small
consistency win that ties the section together visually.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The live-status batch poller (60s cadence, every streamer in the
watch list) was sending two things on every tick:
- `changes` — the diff vs. the previous tick, used by the renderer
- `snapshot` — the full Map<login, boolean> serialized as a record
Renderer destructures only `changes` (renderer-streamers.ts line 20).
The snapshot field was wire-noise. For a typical 30-50 streamer
watch list, that snapshot is ~1.5KB of JSON every minute, never
read on the other side. Dropped from the broadcast payload.
Initial-state sync still works: the renderer's
initLiveStatusSubscription calls window.api.getLiveStatusSnapshot()
once at boot to pre-fill its map. The broadcast is only for diffs.
Also added a short-circuit on the main side: if changes.length === 0
(every streamer's live status matched the cached value this tick),
don't broadcast at all. The renderer would just iterate an empty
array and trigger a no-op render; saves the wakeup entirely.
Type signature updates ride through preload.ts +
renderer-globals.d.ts so the API contract stays accurate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bottom status-bar version span had "v4.1.13" hardcoded as initial
content from a release several versions ago. init() overwrites it via
window.api.getVersion() on app boot, so the user only ever sees this
for the millisecond between paint and IPC return — but during that
window we were lying about the version, and if init() ever crashed
the user would be stuck looking at v4.1.13 in the corner forever.
Cleared the initial content to empty so the span just doesn't render
text until the real version arrives.
Side effect: extracted the inline `style="color:var(--text-secondary);
font-size:12px; margin-left:auto; padding-right:12px"` from
statusBarQueueSummary to a .status-bar-queue-summary class, plus a
new .status-bar-version class for the version pill. Both consistent
with the rest of the status-bar styling. Bonus: queue summary picks
up font-variant-numeric: tabular-nums so the "X B | Y avg | Z done"
numbers don't jitter horizontally as values update during a
download.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First-launch (or after-clearing-everything) opens the app with an
empty sidebar streamer list — just the "Streamer" section heading
and a blank area below. New users had no in-app indication of where
to add their first streamer. The "Add streamer..." input lives in
the TOP bar, which is non-obvious from the sidebar context.
renderStreamers now short-circuits on empty streamers[] and stamps
a small dashed-border hint card into the list with locale-driven
copy pointing the user at the top-right input ("No streamers yet.
Add one via the input at the top right." / "Noch keine Streamer.
Fuege oben rechts einen hinzu.").
The empty state styling (.streamer-list-empty) is intentionally
subtler than the full-page .empty-state used for the VOD grid —
dashed border + tinted background + small padding so it fits the
narrow sidebar rail without dominating it.
Also clears the streamer-section-counter on this branch and hides
the bulk-remove X button, since both would otherwise have stale
state from a previous non-empty render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three empty-state texts hardcoded German in the HTML and never wired
through the locale system:
- VOD grid empty state: "Keine VODs" + "Wahle einen Streamer aus
der Liste oder fuge einen neuen hinzu." Shown when no streamer is
selected. English users were reading German strings here despite
the rest of the app rendering in English.
- Merge tab empty state: "Keine Videos ausgewahlt." Shown in the
Videos zusammenfugen tab before any files are added.
Existing locale tables already had `vods.noneTitle` /
`vods.noneText` / `merge.empty` in both EN and DE — they just
weren't being applied. Added IDs to the three elements
(vodGridEmptyTitle / vodGridEmptyText / mergeEmptyText) and
wired three setText calls in renderer-texts.applyText.
Zero new locale keys; pure plumbing fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
renderEventsList was painting every timeline row with ~7 inline
style props per element (padding, border-bottom, font-size on the
row; margin-right, color on each span; padding-top on detail) AND
keeping a JS-side colour map per event type. For a chatty recording
with 100+ events the inline-style noise added up, and adding a new
event type meant editing the renderer to extend the map.
Extracted:
- .event-viewer-row picks up the padding + bottom border + font-size
- .event-viewer-time gets the secondary colour + monospace stack
- .event-viewer-tag becomes an actual pill (uppercase, letter-
spacing, rounded background tint, bordered) — visually consistent
with the chat viewer's [type] chip tag
- .event-viewer-detail handles the row-detail line spacing
Per-type colour is now driven by CSS [data-type="..."] attribute
selectors (recording_start = green, recording_end = purple,
recording_resume = blue, title_change = amber, game_change = red).
Each variant overrides background + border + text colour to give
each tag a contained "pill" look. The renderer just stamps
ev.type onto data-type and the CSS handles the rest.
Adding a new event type now means one new selector here, not a JS
map edit. Lint, focus, future polish all stay near the styling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five filter-style text inputs (vodFilterInput, streamerListFilter,
archiveSearchQuery, chatViewerFilter) plus three monospace template
inputs (vod / parts / clip filename templates) were each carrying
their own ~80 chars of inline style declaring near-identical
background / border / radius / padding combinations.
Consolidated into three new utility classes:
- .filter-input — base flex-1 minWidth-180 filter look, used by
vodFilterInput
- .filter-input.compact — small variant for the sidebar streamer
filter (smaller padding, smaller font, no flex, percent-width
with margin)
- .filter-input.flex-1-1-240 — larger variant for the archive
search box (240px basis, 200px min, smaller radius/padding to
fit the multi-control form-row it sits in)
- .input-monospace — applies the same monospace stack (Consolas /
Segoe UI Mono / monospace) used by .chat-viewer-time and
.viewer-modal-list-chat to text inputs that hold code-shaped
values
Side effect: vodHideDownloadedToggle had a hardcoded
`accent-color: var(--accent); cursor:pointer;` inline style, which
was redundant after the global custom-checkbox styling landed in
4.6.26 (the checkbox is now ::after-driven, accent-color does
nothing). Removed.
Zero visual change. The inputs render identically because the
class CSS values match what was inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The events-viewer and chat-viewer modals were each carrying ~5 inline
styled elements (modal sizing, status text, list container, filter
row + filter input) duplicated between the two modals. Edits to one
viewer left the other drifting visually.
Extracted to a shared .viewer-modal* family in styles.css:
- .viewer-modal sets the column flex layout
- .viewer-modal-events / .viewer-modal-chat set their own sizing
- .viewer-modal-title / .viewer-modal-status / .viewer-modal-list +
inline + chat list variants for the data area
- .viewer-modal-filter-row + .viewer-modal-filter-input for the
chat viewer's filter
Zero visual change; just stops the two viewers from drifting and
unblocks future polish (skeleton states inside the list, sticky
filter row, etc.) without an inline-edit-by-inline-edit grind.
Side: removed lastArchiveStatsScannedAt module variable in
renderer-stats.ts. It was assigned in refreshArchiveStats but never
read anywhere — leftover from an early plan to compare against a
previous timestamp before refreshing. The renderer-rendered "Last
scan" line reads stats.scannedAt directly. Dead, removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related artifacts from the 4.6.31 a11y pass.
aria-label="Close" was hardcoded English on all five modal-close X
buttons — anyone running the German locale would still hear "Close
button" from their screen reader. Added a shared
.modal-close-localizable class on each X, plus a streamers.modalCloseAria
locale string ("Close dialog" / "Dialog schliessen"), plus a small
setAriaLabelAll helper in renderer-texts that resolves the class via
querySelectorAll and applies the localized label in one shot. Now all
five modals announce in the active language.
While editing the modal headers, also removed the dead "Stream events"
and "Chat replay" English fallback text from eventsViewerTitle and
chatViewerTitle. Both h2s get their textContent overwritten the
instant openEventsViewer / openChatViewer is called (with the
streamers name or a UI_TEXT fallback), so the inline English text was
never user-visible past first-paint and only mattered to a screen
reader if a user managed to focus an unopened modal. Empty <h2/> is
cheaper and removes the i18n drift.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two interrelated changes shipped together.
Clip-cutter modal cleanup. The "VOD zuschneiden" modal was the last
big surface still painted in the apps PRE-purple colour palette:
hardcoded #2b2b2b modal bg, #E5A00D orange title, #1a1a1a slider
tracks (already overridden by the global rule but inline-styles still
sat there), #333 input bgs, #444 borders, plain "white" text, #888
labels, #aaa radio labels. All of it inline. The result: opening the
clip dialog was visually jumping back two themes.
Extracted everything to class-based styles using var() colours:
- .clip-modal* family of classes for layout
- Title now uses var(--text), no orange
- Inputs use var(--bg-elevated) + var(--border-soft) and pick up
the global focus ring automatically
- The duration display ("Dauer: 00:01:00") now sits in a small
green-tinted card to make it visually distinct from the input
rows around it
- Radio labels go through a unified .clip-radio-row with a hover
background tint, and the :has(input:checked) selector swaps the
label text colour + weight when a radio is selected
Global radio button styling. The clip modal had four radio buttons
that were the only non-OS-themed control left in the app. Custom
.appearance:none + ::after-driven dot styling matching the new
checkbox visual: 16px circle, 1.5px border, hover purple tint,
checked fills the inner circle with the accent colour + a soft
purple shadow, focus-visible has the same 3px purple ring as every
other form control. Cascades globally so any future radio gets the
treatment for free.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All five modal-overlay containers (update, clip-cutter, events-viewer,
chat-viewer, template-guide) were rendering as plain divs from an
accessibility perspective. Screen readers would announce nothing
distinguishing when one of them opened, and the close-X buttons would
read as "x button" with no semantic meaning.
Added on each .modal-overlay:
- role="dialog" — tells assistive tech this is a modal region
- aria-modal="true" — instructs the reader to ignore content outside
the dialog while it is open (matches the keyboard escape + click-
outside-to-dismiss behavior the renderer already implements)
- aria-labelledby="<existingTitleId>" — every modal already had a
uniquely-IDd h2; pointed each dialog at its own title so the reader
announces e.g. "Stream events dialog" on open
Added on each .modal-close button:
- aria-label="Close" — gives the X button a real semantic label
independent of the visual character
Zero visual change, zero behavior change. Just makes the app actually
usable for someone running NVDA/JAWS/Orca/VoiceOver. WCAG 4.1.2 +
2.1.1 + 1.3.1 alignment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related artifacts left over from the avatar/banner GQL refactor
in 4.6.20:
- fetchOnlyFollowerCount was an early standalone helper from the
iteration where Helix supplied core profile fields and a separate
public-GQL roundtrip pulled just the follower count. The 4.6.19
rewrite folded all of that into a single public-GQL query, so the
helper has no callers. Removed.
- streamFromPublic was typed via
`Awaited<ReturnType<typeof fetchPublicStreamerProfile>> extends ...`
conditional inference because the inline stream shape was anonymous.
That worked but read like a riddle. Hoisted the inline shapes to
two named interfaces (PublicStreamInfo + PublicStreamerProfileResult)
so the function signature is explicit and the local var is just
`PublicStreamInfo | null`. Same type, an order of magnitude more
obvious to anyone reading.
Both changes are zero-runtime-behavior; tests confirm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bulk-action bar (the purple-tinted row that appears between the
VOD filter and the grid when 1+ VOD checkbox is ticked) was 100%
inline-styled in HTML, which meant:
- No animation when it appears — it just popped into existence
- No reusable styling for similar action surfaces later
- Layout debugging meant editing HTML, not CSS
Extracted to a proper .vod-bulk-bar class, plus .vod-bulk-count for
the "N selected" label and a .vod-bulk-spacer for the flex push.
The CSS rule also picks up a 4px soft purple shadow + a slightly
richer gradient background that matches the rest of the purple
surfaces in the app.
Animation: vod-bulk-bar-slide @keyframes fires every time the JS
flips display:none -> display:flex, because @keyframes restart on
each display change. 220ms, cubic-bezier(0.16, 1, 0.3, 1) for a
quick spring landing, 10px translateY-from + opacity 0->1. The
appear feels intentional now instead of jarring.
Disappear (display:flex -> none) still snaps because CSS cannot
transition through display:none — adding that would require a
class-toggle refactor and an explicit timer to defer the actual
removal. Not worth the complexity for the polish-grade improvement
this is going for.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Active streamer state in the sidebar was a flat purple-tinted
background with a 3px left border. Felt slightly weak as the primary
"this is what you're looking at" affordance, especially with the
similarly-tinted hover state immediately next to it.
Bumped to:
- Gradient background fading purple-strong to purple-faint across
the row, so the active item has a directional emphasis matching
the rest of the Twitch-purple language.
- 1px inset purple ring on top so the active state reads as a
clearly-bordered card, not just a tinted background.
- A small purple right-edge marker (3px wide, 60% tall, centered)
drawn via ::after — mirrors the existing left border and makes
the selected row feel "framed".
- Streamer name in the active row goes 600 weight so the identity
pops over the meta toggles next to it.
Cleanup side: the old generic ::-webkit-scrollbar rule block from
the early days of the app was still in the file at line 1497, even
though the newer purple-themed *::-webkit-scrollbar block further
down has been overriding it for several releases (later wins on
identical specificity). Replaced the old block with a one-line
comment explaining where the live rule lives, so the next person
greping for "scrollbar" doesn't get a misleading hit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two leftover form-control oddities from the audit.
Range sliders (used in the clip-cutter modal and any future
slider-group settings) had a stale orange thumb colour (#E5A00D) from
when the app was a different shade of Twitch. Reskinned to the
current purple family: track gets a subtle purple-to-dark gradient
that visually echoes the queue progress bar, thumb is a 16px purple
circle with a 2px white border and a soft shadow, hover scales the
thumb 1.15x and turns the shadow into a purple halo for the "I can
grab this" affordance. Focus-visible adds the same 3px purple ring
the rest of the form controls use, so keyboard tabbing through a
modal lands on a clearly-focused slider. Mirrored ::-moz-range-thumb
+ ::-webkit-slider-thumb so Firefox and Chromium-Electron look
identical.
Number inputs got the OS spinner stack hidden globally
(::-webkit-inner-spin-button / outer + -moz-appearance: textfield).
The default Webkit spinners are a tiny gray arrow pair that always
reads as "unfinished" and clutter the look. Users still get keyboard
arrow keys + wheel scroll. If a custom spinner pattern is needed
later it can come back as a chrome around the input — for now the
inputs read clean as text fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit turned up 20 raw `<input type="checkbox">` in Settings still
rendering with OS-default gray-square look — and most `<select>`
elements showing the OS-default dropdown arrow. With the rest of the
UI now Twitch-themed (purple inputs, modal pops, animated everything),
those felt jarringly out of place.
Checkbox: 16px rounded square, dark base with 1.5px border, hovers to
a purple-tinted border, fills purple + draws a CSS-only white check
on the diagonal when checked, scales down briefly on click, focus
shows the same 3px purple ring as the text inputs. No JS, just
:checked + ::after.
Select: appearance:none everywhere to kill the OS chevron, then an
inline-SVG chevron in background-image at right:8px (gray default,
purple on hover). padding-right boosted to 28px so option text never
overlaps the arrow. The dropdown menu itself still uses the OS list,
but the closed control matches the rest of the input family now.
Cascades globally via input[type="checkbox"] / select selectors — no
markup edits needed. The few selects/checkboxes that previously had
inline accent-color overrides keep working because accent-color
applies to native widgets, which we have now replaced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-4 polish.
- Streamer section counter. Tiny line next to the "Streamer" sidebar
heading: "12" when nobody is live, "12 · 3 live" with the live
count highlighted red when broadcasters from the watch list are
on air. Re-rendered on every renderStreamers call so it stays in
sync with add/remove and the 60s live-status poll.
- VOD duration badge. Twitch-style bottom-right pill on every VOD
thumbnail showing the recordings duration ("32h37m9s"). 11px,
white-on-near-black, 2px backdrop-blur, hover deepens the
background, fades out when the storyboard preview activates so
the preview frame reads cleanly. Pairs with the existing
downloaded checkmark badge (top-left) and live-recording badge
to give each thumbnail a complete at-a-glance status row.
- Queue progress bar shimmer. The fill bar now uses a purple-to-
light-purple gradient and rides a moving white-translucent
highlight strip that sweeps L->R every 1.8s. Same translateX-100%
to 100% trick used everywhere else, but only visible because
the underlying bar has colour. Makes "currently downloading"
obvious without needing a separate spinner.
- Chat viewer polish. Replaced the inline per-message styling with
proper .chat-viewer-* classes: hoverable row background, system
events (subs/raids/deletions) get a left-purple-border + tinted
background to set them apart from normal chat lines, the type
tag (e.g. [sub], [raid]) renders as a real chip with a border,
timestamps are mono-fonted and faded. Per-user IRC colour from
Twitch metadata is still respected as an inline override on the
username.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Polish round 3, plus a class-collision fix.
Fix first: the new X-close button class introduced in 4.6.21 was
called .btn-icon, which collided with an EXISTING .btn-icon class
already used by the top-bar Refresh button (and elsewhere). The
second declaration partially overrode the first, leaving Refresh
with the wrong hover state (red instead of purple-tinted). Renamed
the close-button class to .btn-close and updated the two call sites
(btnStreamerBulkRemove + vodFilterClearBtn). Refresh now hovers
correctly with a purple tint + a 180deg SVG icon spin on hover.
Polish bundle:
- Input focus ring globally: every text/search/number/password
input + textarea + select picks up a 3px rgba purple ring on
focus, with a smooth 180ms transition on border-color, box-shadow,
and background. Focus state finally reads as intentional instead
of the OS default blue glow.
- Queue items: 3px left border that color-codes by status (purple
while downloading, green when complete, red on error), faint
purple-tinted background when downloading, soft glow on the
status dot. The queue list now reads as a status timeline at a
glance.
- Top-bar Refresh button picks up a 1px border, purple-tint hover
background, and the SVG arrow spins on hover for the "refresh"
feel.
- Header search box (Add streamer): consistent border-radius (6px
vs 4px) and the + button gets a hover shadow + active-press
micro-bounce.
- App toast: gradient background, accent-color left border (purple
for info, amber for warn, red for error), animated slide-in from
the right instead of vertical, backdrop blur for content
legibility over busy backgrounds, and an extra .error variant
class. Feels modern instead of like a notice strip from 2010.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>