Commit Graph

94 Commits

Author SHA1 Message Date
xRangerDE
70643b4c08 a11y: aria-live=polite on clipStatus + chatViewer/eventsViewer status
Three more user-triggered status messages that updated silently for screen-reader users:
- clipStatus — clip download progress/result text below the URL input on the Clips tab
- eventsViewerStatus — loading/error message inside the events viewer modal
- chatViewerStatus — loading/error message + filter result count inside the chat viewer modal

All three get role="status" + aria-live="polite". Same reasoning as 4.6.153/154 — the user explicitly clicks a button or opens a modal, then the result fills in asynchronously. Without aria-live the announcement was lost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:16:49 +02:00
xRangerDE
ae156ff395 a11y: aria-live=polite on 3 more refresh-result status messages
Following the cleanupReport fix in 4.6.153, three more user-triggered refresh result strings were updating silently:

- statsLastScannedLabel — "Letzter Scan: <timestamp>" / "Scanne..." in the Archive-Statistik header after clicking Aktualisieren
- archiveSearchSummary — "{matchCount} matches (scanned {scanned}…)" / "Scanne..." after clicking Suchen
- storageSummary — "Total: {files} files, {size} — Free disk: {free}" / "Scanne..." after clicking Aktualisieren on the Storage card

All three are textContent updates triggered by an explicit user action that finishes asynchronously. Without aria-live, screen reader users hear nothing after pressing the action button — the result text fills in off-screen.

Added role="status" + aria-live="polite" to all three. "polite" because the result isn't urgent — the user requested it and waiting for a natural break in speech is fine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:11:13 +02:00
xRangerDE
25be77b4ab a11y: cleanupReport gets role=status + aria-live=polite
The Auto-Cleanup card has a "Vorschau" (dry-run) and "Jetzt ausfuehren" button pair followed by a cleanupReport div that displays the result text — "Wuerde X Dateien aelter als Y Tagen verschieben", "X Dateien archiviert", or "Fehler". Without an aria-live region, screen reader users who click the button never hear what happened: the focus is still on the button, the result text appears off to the side silently.

Added role="status" + aria-live="polite" so the report's textContent change gets announced when it's set. "polite" because the report isn't an alert/error — the user requested the action, so they can wait for the screen reader's natural break in current speech to announce the result.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:06:40 +02:00
xRangerDE
b880ce9694 ui: .select-compact hover state + drop redundant margin:0 on filenameTemplatesTitle
Two small targeted polishes:

1. .select-compact had no :hover state — selects in the VOD sort, Archive filters, and other compact dropdowns gave no mouse-feedback visual when pointed at. Added a soft purple-accent tint on hover (background + border-color transition) matching the rest of the app's interactive-control hover palette. The global select:focus rule already handles keyboard focus; this only adds the mouse-hover affordance.

2. The filenameTemplatesTitle label carried style="margin: 0;" inline. The global * { margin: 0 } reset at the top of styles.css already zeros every element's margin by default, so the inline declaration was a literal no-op. Dropped it — same noise-removal as the archiveTitle fix in 4.6.99.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:48:03 +02:00
xRangerDE
00e366ce50 cleanup: .form-row.search-bar + .select-compact.size-md modifiers
The Archive-search tool-row carried two inline-style attrs:
- .form-row with gap:8px; margin-bottom:8px; flex-wrap:wrap; align-items:center (different rhythm from the existing .aligned / .section-header form-row modifiers — tool-row that should wrap on narrow widths)
- archiveSearchStreamer .select-compact with min-width:160px (matches the existing .form-stack.size-md width)

Extracted into two modifier classes:
- .form-row.search-bar — the search/filter tool-row pattern (search input + filter selects + go button collapse gracefully)
- .select-compact.size-md — wider select variant, mirrors the .form-stack.size-md pattern from 4.6.95 so width modifiers across element types share the same naming

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:43:35 +02:00
xRangerDE
dd5efcbfe6 cleanup: finish .is-hidden migration in renderer-updates — all .style.display gone
Completes the renderer-updates display-toggle migration started in 4.6.137. Five more elements (updateChangelogCard, updateModalSkipBtn, updateModalMeta, updateProgress in two more code paths) were still using inline .style.display / inline display checks. Each has been switched to:
- classList.add/remove/toggle('is-hidden') for writes
- classList.contains('is-hidden') for the two state-reads (refreshUpdateChangelogToggleText, toggleUpdateChangelog)

Plus the two remaining inline style="display:none;" HTML attributes (updateModalMeta + updateChangelogCard) moved to class="... is-hidden".

After this: renderer-updates.ts has zero .style.display references — the entire update-banner + update-modal visibility surface is class-driven.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:38:07 +02:00
xRangerDE
b33b274751 cleanup: update banner + progress use class-based visibility (.show / .is-hidden)
The .update-banner CSS has had a .show modifier (display:flex) defined since the original auto-updater code, but the JS was bypassing it and setting .style.display='flex'/'none' directly. Three places had this inconsistency:
- showUpdateBanner() — set inline display:flex
- hideUpdateBanner() — set inline display:none
- The check at line 461 — read inline display === 'flex' to gate a "no update found" toast

Switched all three to the canonical .show class — adding/removing it and checking with classList.contains. The CSS rule does the actual display flip.

The updateProgress wrap (download progress bar inside the banner) was using the same .style.display = 'block' / 'none' inline-toggle pattern in three places (setUpdateBannerAvailableUi, setDownloadPendingUi, setDownloadReadyUi). Migrated all three to the shared .is-hidden class from 4.6.134, plus removed the inline style="display: none;" from the HTML.

Net: 5 inline display assignments + 1 state read + 1 inline HTML display:none gone, update-banner state machine is now fully class-driven.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:32:57 +02:00
xRangerDE
3788561bb7 cleanup: streamerProfileHeader uses .is-hidden + .streamer-profile-skeleton owns display:flex
The profile header on the VOD tab cycled between three display states via inline .style.display assignments — 'none' when no streamer is selected, 'flex' while loading (skeleton), 'block' once profile data is back. Three separate inline writes plus a starting style="display:none" in HTML.

Two changes:

1. Baked display:flex into the .streamer-profile-skeleton CSS rule itself (was previously implicit via the JS flip). Now the skeleton class fully owns its layout — adding it switches the element to flex, removing it falls back to .streamer-profile-header's base display:block.

2. Replaced the inline 'none' assignment with the shared .is-hidden class (from 4.6.134). hideStreamerProfileHeader adds it; renderStreamerProfileSkeleton + renderStreamerProfileCard both remove it as part of their existing classList work. HTML drops the inline display:none too.

Three .style.display assignments + one inline display:none gone, profile header's visual state is now entirely class-driven.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:26:00 +02:00
xRangerDE
78c6df0d6b cleanup: streamerListFilter + btnStreamerBulkRemove use .is-hidden
Continuing the .is-hidden migration from 4.6.134 — two more sidebar-shell elements were doing the same .style.display = '' / 'none' dance:

- streamerListFilter input (compact filter that only appears once the streamer list crosses STREAMER_FILTER_THRESHOLD)
- btnStreamerBulkRemove button (X bulk-remove button, same threshold gate, plus a separate hide path inside the no-streamers branch)

Both started with style="display:none" in HTML and toggled via .style.display in renderer-streamers.ts. Now use the shared .is-hidden class — HTML drops the inline style, JS uses classList.toggle/add.

3 more .style.display assignments + 2 inline display:none HTML attrs gone, identical behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:20:27 +02:00
xRangerDE
7994a02bb1 cleanup: .is-hidden utility — replaces 3 toggle-display patterns across queue + streamers
Three runtime-toggled elements were each using a slightly different inline display value to flip between visible/hidden:
- vodFilterClearBtn: button, .style.display = '' / 'none'
- btnMergeGroup: button, .style.display = '' / 'none'
- vodBulkBar: div, .style.display = 'flex' / 'none'

Plus each carried an inline style="display:none;" in HTML to start hidden.

Added a single .is-hidden utility class (display:none !important) that hides any element regardless of its natural display type, and:
- HTML now uses class="... is-hidden" instead of style="display:none"
- JS toggles with classList.toggle('is-hidden', !shouldShow) instead of poking at .style.display
- .vod-bulk-bar gets an explicit display:flex in its base rule (was implicit via the JS flip; now declared)

Comment on .vod-bulk-bar animation updated since the trigger is now ".is-hidden removed" rather than "JS sets display:flex".

Net: 3 inline style="display:none" attrs gone from HTML, 4 .style.display assignments gone from TS. Single utility class handles the same job for all three plus any future show/hide toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:16:21 +02:00
xRangerDE
5473a852ee cleanup: .cutter-info + .timeline-container .shown modifiers — 4 inline display references gone
Both the cutter-info bar (resolution/duration/fps/selection readouts) and the timeline-container (timeline + start/end inputs) were hidden by default via inline style="display:none;" in HTML, then loadCutterFromPath() set .style.display = 'flex' / 'block' once a video was loaded.

Moved both into CSS with the same pattern as 4.6.126 (.queue-details.expanded) and 4.6.132 (.clip-template-wrap.shown):
- Base .cutter-info / .timeline-container rule sets display:none
- .shown modifier flips to flex / block respectively (preserves the original visible-state layout)
- HTML drops the inline style attribute
- JS uses classList.add('shown') instead of poking at .style.display

Four inline-style references gone (2 HTML + 2 TS), no behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:10:32 +02:00
xRangerDE
479e861789 cleanup: .clip-template-wrap.shown modifier — kills inline display:none + style.display toggle
The clip-cutter modal's custom-template wrap (.clip-template-wrap) was hidden by default via inline style="display:none;" in HTML and shown/hidden by updateFilenameTemplateVisibility() via wrap.style.display = 'block' / 'none' based on the selected filename format.

Moved both into CSS: the base rule now sets display:none, and a .shown modifier flips to display:block. The renderer toggles the class via classList.toggle('shown', ...) instead of poking at .style.display, and the HTML drops its inline style attribute.

Same pattern as 4.6.126 (.queue-details.expanded). Two inline style references gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:04:25 +02:00
xRangerDE
773addb279 a11y + i18n: chat viewer filter placeholder + aria-label, status dot aria-hidden
Two unrelated small fixes bundled:

1. The chat viewer modal's filter input (chatViewerFilter) had a hardcoded "Filter..." placeholder that was never localized — every other filter input in the app routes its placeholder through UI_TEXT. Added queue.chatViewerFilterPlaceholder + queue.chatViewerFilterAria locale keys (DE: "Chat filtern..." / "Chatnachrichten filtern"; EN: "Filter chat..." / "Filter chat messages") and wired them through renderer-texts so the placeholder now matches the active language and screen readers get a proper accessible name on the input.

2. The status-bar's coloured dot (statusDot) had no aria-hidden — screen readers would read it as a generic element with no meaning. The status text right next to it already carries the same information ("Verbunden" / "Nicht verbunden"), so the dot is purely decorative. Added aria-hidden="true".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:49:17 +02:00
xRangerDE
5ea763d79b defensive: type="button" on all 43 <button> elements in static HTML
The HTML had 59 <button> elements; 16 already declared type="button" but the remaining 43 relied on the browser default. The default for <button> without a type attribute (and outside a <form>) is type="submit" — harmless today because index.html has no <form> elements, but the moment a future refactor wraps any section in a form, every unmarked button suddenly becomes a submit button. That's a latent footgun.

Added type="button" to all 43 unmarked buttons via a regex replacement. Now any <button> in the markup is explicitly non-submit — no behaviour change today, but the markup no longer depends on the absence of a form element to behave correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:31:53 +02:00
xRangerDE
7b6ced7818 a11y: aria-hidden on all 12 decorative SVG icons
Every <svg> in index.html is a decorative graphic that visually reinforces an adjacent text label — the logo SVG sits next to "Twitch VOD Manager", the 7 nav-item SVGs each pair with their tab name, the refresh button SVG sits next to "Aktualisieren", and the 3 empty-state SVGs sit above a heading + paragraph. The graphics carry no information that isn't already in the surrounding text.

Without aria-hidden, screen readers either announce these as "image" / "graphic" (depending on implementation) or silently traverse them — neither adds value, and the former adds noise to nav navigation in particular.

Added aria-hidden="true" to all 12 SVG elements in static HTML. Screen readers now skip them and read just the labelled text, which is the desired UX for decorative icons.

(The two existing aria-hidden="true" attributes on the flag-icon spans in the language picker were already correct and are not affected.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:12:12 +02:00
xRangerDE
42c7831c96 a11y: scope=col + aria-labelledby on the template-guide variable table
The "Verfugbare Variablen / Available Variables" reference table inside the Template Guide modal (a 3-column data table mapping placeholders to descriptions and example output) was missing two standard data-table a11y hooks:

- The three <th> cells had no scope attribute. The implicit <th>-inside-<thead> column scope works in modern screen readers but explicit scope="col" is the recommended pattern and what a11y audit tooling looks for.

- The <table> had no programmatic relationship to its own heading (the <h3 id="templateGuideVarsTitle"> directly above). Added aria-labelledby="templateGuideVarsTitle" so screen readers announce the table's purpose ("Verfugbare Variablen, Tabelle") when the user enters it.

Pairs nicely with the storage-stats table fix in 4.6.107 — both data tables in the app now have explicit column scopes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:01:26 +02:00
xRangerDE
a7fbdd2fbf a11y: language-picker is now a labelled button group for screen readers
The language picker is a pair of <button aria-pressed=...> toggles (Deutsch / English) sitting inside a div, with a separate <label id="languageLabel">Sprache</label> directly above. Two issues:

- The <label> has no for= attribute because the actual control is a custom button-pair, not a single input. So the label was just floating text — present visually but not programmatically connected to the buttons it describes.

- The two buttons were a flat list with no group container, so a screen reader navigating button-by-button announced "Deutsch, pressed" / "English, not pressed" without any context that these are language alternatives.

Added role="group" and aria-labelledby="languageLabel" to the language-picker div. Screen readers now announce "Sprache, group, Deutsch button pressed" — the floating label becomes the group's accessible name and the relationship is exposed via the accessibility tree.

Kept the aria-pressed toggle pattern (rather than rewiring to role="radiogroup"+role="radio") because the JS state plumbing already aligns with the toggle semantics and the group label is enough to clarify the picker's intent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:28:13 +02:00
xRangerDE
3e5bdb73d4 dead-code: remove .clip-template-lint orphan + extract clipTemplateGuideBtn margin
The .clip-template-lint CSS rule was documented as a no-op alias kept in case any external reference still used it (deprecated when the shared .template-lint class with .ok/.warn modifiers took over). Grep confirms zero external references — both .ts files and index.html only mention the new .template-lint class. Deleted the rule + its 5-line comment.

Same edit moves the clipTemplateGuideBtn's margin-top:8px from inline to a .clip-template-wrap .btn-secondary descendant rule. One inline style attribute gone, and the spacing now lives next to the .clip-template-wrap definition where future readers will look for it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:22:36 +02:00
xRangerDE
8f6d7b2d9a cleanup: .section-header-actions class + drop redundant margin-top:0 on archiveTitle
The stats card header (Archiv-Statistik) had an inline-styled inner div grouping the "last scanned" label with the Aktualisieren button — display:flex; gap:8px; align-items:center;. Extracted that pattern to .section-header-actions, which has a real semantic role: it's the right-side action cluster inside a section-header that needs to stay together as a single flex item so the parent's justify-content:space-between can pin it to the right.

Same edit drops the inline style="margin-top:0;" from <h3 id="archiveTitle"> — the global * { margin: 0 } reset already zeroes it, so the inline declaration was a literal no-op that just added noise to the markup. Worth removing as it might mislead future readers into thinking the override does something.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:17:09 +02:00
xRangerDE
f6bb5970c9 cleanup: mergeDesc + versionInfo reuse .card-intro — typography consistency
Two more inline-styled secondary paragraphs converted to the shared .card-intro class:

- mergeDesc (Merge tab's intro paragraph): was color/margin-bottom:15px inline
- versionInfo (Updates card's "Version: v4.1.13" line): was color/margin-bottom:10px inline

Both sit below an h3 inside a settings-card — the same structural role as the 7 other .card-intro paragraphs. Converting unifies font-size (was browser-default ~14-16px, now the consistent 13px secondary-text size) and margin rhythm (12px) with the rest of the app's card intros. Visual delta is sub-perceptible (a few px in margin, ~1-3px in font-size) but the markup now reads its intent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:12:08 +02:00
xRangerDE
38aadb6fb9 cleanup: .template-lint owns its own margin-top — unifies two divergent inline values
Both template-lint usages (clip-cutter modal at index.html:105 and Settings filename-template card at index.html:657) had different inline margin-top values — 4px vs 6px. The visual difference is essentially imperceptible but it's a pointless divergence: the same class, same context (a lint badge directly below a template input), spaced differently for no design reason.

Hoisted margin-top:6px into the .template-lint base class. Both usages now drop their inline override and pick up the same rhythm. Clip-cutter's lint shifts 2px down — visually identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:06:03 +02:00
xRangerDE
afbd09f507 cleanup: .settings-card.centered + .info-text + .stats-summary-grid
Three more inline-styled blocks moved into CSS classes:

- The Clips Info card had max-width:600px; margin:20px auto inline on the settings-card itself, plus color/line-height/white-space:pre-line inline on the info paragraph. Now .settings-card.centered (modifier for narrow-centred cards) and .info-text (multi-line description text with preserved authored line breaks).

- The Stats Summary KPI grid carried a 3-property grid declaration inline (display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:12px). The layout is unique to this one container, but lifting it to .stats-summary-grid makes the markup self-documenting and matches the rest of the .stats-* class family.

Net: 3 inline style attributes gone, 3 new CSS rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:01:08 +02:00
xRangerDE
3362138d1a cleanup: .form-stack size modifiers + .input-narrow — 5 inline width styles gone
Two related size-modifier patterns extracted:

1. .form-stack.size-sm (min-width:120px) and .form-stack.size-md (min-width:160px) — the Auto-Cleanup 3-up row carried three near-identical inline min-width declarations (120 / 160 / 160). They control where the row breaks to a new line on narrow widths, so the breakpoint metadata is logically a layout-modifier-class concern, not inline-style markup.

2. .input-narrow (width:90px) — the two Auto-VOD inputs (Poll-Intervall, Max. Alter) had their width inline because the values are 2-3 digits and a full-width input would look odd. Encoded once as a class, applied twice.

Net: 5 inline style attributes gone, 3 new CSS rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:56:31 +02:00
xRangerDE
2c0c7f6d00 cleanup: .card-intro a default colour + archive-search-summary uses .form-sublabel
The Twitch-API card's help-text paragraph contains one inline link (apiHelpLink → dev.twitch.tv/console/apps). It carried color/text-decoration/cursor inline — but text-decoration:underline and cursor:pointer are the browser defaults for <a href=...>, so only the accent colour was actually doing work. Hoisted that to a single .card-intro a descendant rule — any future inline link inside a card intro paragraph picks up the accent colour without ceremony.

Same edit converts the archiveSearchSummary div from inline (font-size:12px; color:var(--text-secondary)) to the existing .form-sublabel class — exact match.

Two more inline style attributes gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:51:13 +02:00
xRangerDE
5931892320 cleanup: merge tab uses pre-existing .merge-empty-state class + clip-cutter placeholder gets descendant rules
The merge tab's empty state at index.html:400 was written as <div class="empty-state" style="padding: 40px 20px"> with two more inline-styled descendants (SVG opacity:0.3 and p margin-top:10px). A .merge-empty-state class with exactly those three properties has been sitting in styles.css since the original empty-state pass — the HTML just never picked it up. Switched to <div class="empty-state merge-empty-state"> + bare children. 3 inline styles gone.

Same pattern in the clip-cutter video preview placeholder at index.html:332-333 (svg opacity:0.3, p margin-top:10px). Added two descendant rules to the existing .video-preview .placeholder block so the styling lives in CSS. 2 more inline styles gone.

Net 5 inline style attributes gone, no visual change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:47:13 +02:00
xRangerDE
6946c34395 a11y: 14 more labels linked to their inputs via for=
Continuing the label-for pass from 4.6.79 + 4.6.83. The Settings tab, Twitch-API card, Discord card, Clips template guide, and the main cutter time-input row all had label elements sitting right next to their inputs/selects with no programmatic for=/id pairing — screen readers couldn't announce the label on focus, and clicking the label text didn't focus the control.

Fixed labels (label-id → target-input-id):
- themeLabel → themeSelect
- clientIdLabel → clientId
- clientSecretLabel → clientSecret
- storageLabel → downloadPath
- modeLabel → downloadMode
- partMinutesLabel → partMinutes
- parallelDownloadsLabel → parallelDownloads
- streamlinkQualityLabel → streamlinkQuality
- performanceModeLabel → performanceMode
- metadataCacheMinutesLabel → metadataCacheMinutes
- discordWebhookUrlLabel → discordWebhookUrl
- templateGuideTemplateLabel → templateGuideInput
- cutterStartLabel → startTime
- cutterEndLabel → endTime

Together with the prior passes (clip-cutter modal labels in 4.6.79, filename-template labels in 4.6.83), every plain label-above-input pair in static HTML now uses for=. Remaining unattached labels (languageLabel, vodHideDownloadedLabel) are intentional — the first targets a custom button-pair language picker, the second wraps its own checkbox via .inline-toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:43:01 +02:00
xRangerDE
3ec88a7800 a11y: aria-label on the 3 icon-only buttons in the main shell
Three icon-only buttons in the always-visible UI had no programmatic accessible name:
- the "+" add-streamer button (no id, no title, no aria-label — entire button was unnamed)
- the bulk-remove X button next to the Streamer section title (had a localized title, but title is not a reliable accessible-name source — many screen readers don't expose it)
- the VOD-filter clear X button above the VOD grid (same — title-only)

For all three the visible text is just a glyph ("+", "x"), so the accessible name has to come from somewhere else. Added:
- new locale key static.streamerAddAriaLabel ("Streamer hinzufuegen" / "Add streamer") and an id on the "+" button so renderer-texts can localize it
- new setAriaLabel(id, value) helper in renderer-texts mirroring the existing setTitle/setPlaceholder pattern
- aria-label calls for all three buttons, in addition to the existing title (so the tooltip stays for sighted users)

The two existing X buttons reuse their existing title strings as aria-label — no new translation work, just exposing the already-present text via the right ARIA attribute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:37:26 +02:00
xRangerDE
b3d77040de cleanup: .form-row.aligned modifier + .form-sublabel reuse — 5 inline styles gone
Three Settings cards (Debug-Log, Auto-VOD-Download, Runtime Metrics) carried the identical inline style attribute on their action-row form-row:

    style="margin-bottom: 10px; align-items: center;"

Extracted into a single .form-row.aligned modifier — three duplicated inline declarations collapsed to one shared rule that pairs naturally with the existing .form-row.section-header pattern.

Two more inline styles converted to the existing .form-sublabel class:
- autoVodStatusLine: style="font-size:12px; color: var(--text-secondary);" — exact match for .form-sublabel
- storageSummary: same as above plus margin-bottom:8px (kept inline since margin is a one-off positioning concern, not part of the sublabel concept)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:31:40 +02:00
xRangerDE
ed20c44749 cleanup: settings-card h4 + hr defaults — and Auto-Cleanup label reuses .toggle-row
The Auto-Cleanup subsection inside the Storage card carries three inline-styled patterns that are good candidates to live in the cascade rather than the markup:

- <hr> with border:none; border-top:1px solid var(--border-soft); margin:16px 0
- <h4> with margin:0 0 8px 0; font-size:14px
- a checkbox <label> wrapping the "Auto-Cleanup aktivieren" toggle with the same display:flex; align-items:center; gap:8px pattern that the existing .toggle-row class already encodes

Hoisted the hr + h4 styling to default .settings-card descendant rules — any future subsection divider/heading inside a settings card now follows the same rhythm without ceremony — and the label reuses .toggle-row (margin-bottom:8px stays inline because the next sibling is a form-row, not another toggle-row, so the .toggle-row + .toggle-row sibling rule doesn't apply).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:27:05 +02:00
xRangerDE
e61940c108 cleanup: extract .section-title flex layout + .section-title-label
The sidebar's streamer section-title carried two inline flex containers:
- outer .section-title with display:flex; align-items:center; gap:6px; justify-content:space-between (so the bulk-remove X button can pin right while the title group stays left)
- inner span with display:flex; align-items:baseline; gap:8px (so the title text and the streamer counter share the same text-baseline rather than centring against each other)

.section-title is only used in this one spot, so the flex layout becomes part of the class definition (no risk of bleeding into other usages). The inner-span pattern moves to a dedicated .section-title-label class. Two inline style attributes gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:17:16 +02:00
xRangerDE
35dc3201d8 cleanup: stats card header reuses .form-row.section-header + .card-intro.flush
The Statistics tab's "Archiv-Statistik" card had its own bespoke inline-styled flex header (display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px) plus an inline margin:0 on the h3 and an inline-styled intro paragraph with a different margin rhythm than the other 7 card intros (margin-top:8px; margin-bottom:0).

Two small additions reuse the existing classes:
- flex-wrap:wrap added to .form-row.section-header so the System-Check / Storage / now-Stats headers all wrap gracefully on narrow widths instead of overflowing. Strict improvement everywhere it's already used.
- .card-intro.flush modifier (margin-top:8px; margin-bottom:0) for intros that sit flush against the next block rather than spaced from it.

Three more inline style attributes gone, and the stats header now shares the same width-collapse behavior as the other section-header rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:12:10 +02:00
xRangerDE
ccfff174ae cleanup: .form-row.section-header + reuse .inline-toggle — kill 6 inline styles
Two recurring inline-style patterns in Settings:

1. The "card title + right-aligned refresh button" header — used by the System-Check card and the Storage card. Both carried the identical 3-property inline style on the form-row plus an inline margin:0 on the h3 inside. Now expressed as .form-row.section-header (with a descendant h3 margin reset) — two cards, four inline attrs gone.

2. The "checkbox + Auto-Refresh label" pattern next to the debug-log and runtime-metrics action buttons — both were inline-styled with display:flex; align-items:center; gap:6px; font-size:13px; color:var(--text-secondary). The existing .inline-toggle class (already used by the VOD filter row's Hide-downloaded toggle) is the exact same pattern at 12px instead of 13px — close enough that unifying onto the shared class is the right move. 1px down beats keeping a third near-duplicate definition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:07:51 +02:00
xRangerDE
534f22b632 cleanup: extract .card-intro CSS class — kill 7 duplicated inline styles
Seven settings/feature cards (Archive, API help, Storage, Cleanup, Discord, Auto-VOD, Backup) each carry an intro paragraph with the exact same inline style attribute:

    style="color: var(--text-secondary); font-size:13px; margin-bottom:12px; line-height:1.5;"

That's the same 4-property declaration duplicated 7 times. Extracted into a single .card-intro class — HTML reads as semantic intent ("this is a card-intro paragraph") instead of repeated style soup, and any future tweak to intro-paragraph styling now lives in one place.

(statsIntro is similar but uses margin-top:8px; margin-bottom:0; — different rhythm because it sits above the stats grid, left as-is.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:03:09 +02:00
xRangerDE
0e313e8857 cleanup+a11y: .filename-template-grid CSS class + for= on the 3 template labels
The filename-templates 3-pair grid in Settings carried four inline style attributes:
- the wrapper div's display:grid; gap:8px; margin-top:8px
- three labels with the same font-size:13px; color:var(--text-secondary), with the 2nd and 3rd also having margin-top:4px

Extracted into a single .filename-template-grid class block (with descendant label styling + a :not(:first-child) margin-top rule). HTML drops from a noisy block of inline styles to a clean labelled grid.

Same edit added for= associations on the three labels (vodTemplateLabel → vodFilenameTemplate, partsTemplateLabel → partsFilenameTemplate, defaultClipTemplateLabel → defaultClipFilenameTemplate) — they were sitting right next to their inputs with no programmatic association. Continues the label-for a11y work from 4.6.79; clicking a label now focuses its input and screen readers announce the label when the input is focused.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:58:39 +02:00
xRangerDE
bbdcf8f71c cleanup: extract .vod-filter-row CSS class — kill 3 inline style attrs
The filter row above the VOD grid carried three inline style attributes:
- the row's own flex layout (display:flex; align-items:center; gap:8px; margin-bottom:12px; flex-wrap:wrap)
- vodSortLabel had margin-left:8px (extra spacing past the row's gap to visually group "Sort:" with the select)
- vodFilterCount had min-width:80px (prevents layout shift as count text changes during typing)

All three are now CSS class definitions (.vod-filter-row, .vod-sort-label, .vod-filter-count). HTML reads cleaner and the styling lives alongside the related .vod-bulk-bar block in styles.css.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:43:47 +02:00
xRangerDE
5641924c7e a11y: clip-cutter + auto-vod settings labels linked to inputs via for=
Five <label> elements in the clip-cutter modal and two <span class="form-sublabel"> elements in the Auto-VOD settings card had no for=/id pairing with their associated form controls — screen readers couldn't announce the label when the input was focused, and clicking the label didn't focus the input.

Fix: added for= to 5 clip-modal labels (Start/StartTime/End/EndTime/Part) and converted the two Auto-VOD sublabels from span to label for= so screen readers correctly associate them with autoVodPollMinutes and autoVodMaxAgeHours.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:41:14 +02:00
xRangerDE
c393457492 a11y: update-banner progress bar role=progressbar + aria-valuenow
Third progress bar in this a11y pass — the download-progress bar
shown in the update banner during an auto-update download. Same
pattern as 4.6.64 (queue) + 4.6.65 (cut/merge): bare div with
JS-driven width, no semantic role.

Promoted the .update-banner-progress-track to role="progressbar"
with aria-valuemin / max / now + a localized aria-label
(updateProgressAria: "Update download progress" / "Update-Download-
Fortschritt").

Three call sites in renderer-updates.ts that drive bar.style.width
now also stamp aria-valuenow on the gauge:
- onUpdateProgress event handler (per-tick percent)
- setDownloadPendingUi (initial 30% indeterminate placeholder)
- setDownloadReadyUi (100% on finish)

renderer-texts.applyText sets the localized aria-label at boot +
on language switch.

That's all three application-level progress bars now AT-friendly.
The same pattern would extend to any future progress UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:36:42 +02:00
xRangerDE
fa440951d2 a11y: cut + merge progress bars role=progressbar + aria-valuenow
Following 4.6.64 for the queue progress bars, the cut + merge
progress containers in their respective tabs had the same gap:
a plain <div class="progress-bar"> wrapping a <div class="progress-
bar-fill"> with no semantic role. JS poked the bar's style.width
on every percent update; AT had no way to read out the running
value.

Promoted both .progress-bar wrappers to role="progressbar" with
aria-valuemin / max / now, plus aria-label sourced from new
locale strings (cutProgressAria / mergeProgressAria) so EN/DE
both work.

The progress event handlers in renderer.ts now also stamp
aria-valuenow on each tick, so AT live regions pick up the
percentage as the cut / merge advances. setAttribute is cheap
relative to the FFmpeg progress event rate (~1/s), no perf
concern.

renderer-texts.applyText sets the localized aria-label on both
gauges at boot + language switch — text contents already get
re-applied through the same path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:31:41 +02:00
xRangerDE
274d3874f5 cleanup: extract .select-compact class — 4 selects outside .form-group
The vodSortSelect + the 3 archive-search selects all sat OUTSIDE
the .form-group container, which meant the existing
.form-group select rule did not apply to them. Each carried the
same ~110 chars of inline style hard-coding bg / border / radius /
padding / color.

Extracted to a shared .select-compact class (6px radius, 7px
padding, var(--bg-card) base, var(--border-soft) border) and
swapped all four call sites. Visual consistency between the
filter-row select in the VODs tab and the multi-select control
strip in the Archive search — same heights, same border treatment.

Minor visual change for the 3 archive selects (4px -> 6px
border-radius, 6px -> 7px vertical padding) so the controls now
match the rest of the apps button + input family. Worth the 1px
delta for the consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:22:16 +02:00
xRangerDE
5fda4e2103 a11y: sidebar nav-items keyboard-accessible + aria-current
All 7 sidebar nav-items (Twitch VODs / Clips / Cutter / Merge /
Statistik / Archiv / Einstellungen) were plain `<div class="nav-item">`
elements with only an onclick. Same a11y story as the previous two
iterations: no role, no tabindex, no semantic active-state marker,
no keyboard activation.

Added on each nav item:
- role="button" and tabindex="0" so they enter the tab order and
  read as activatable buttons to assistive tech
- aria-current="page" applied to the active item, removed from the
  others — both managed in showTab() since that's the single
  switch point for active-state transitions
- A delegated keydown handler on the .nav container (one listener,
  not seven) that fires showTab on Enter / Space for whatever
  nav-item descendant is currently focused. Bound once with a
  data-keynav-bound guard so init() re-running doesn't double-bind

CSS adds a 2px purple focus-visible ring matching the rest of the
keyboard-focus family added in 4.6.50 and 4.6.51.

WCAG 2.1 success criterion 2.1.1 (Keyboard) — every interactive
element activated by keyboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:27:53 +02:00
xRangerDE
ce469b856c cleanup: update-banner progress bar — extract inline styles
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>
2026-05-11 03:07:59 +02:00
xRangerDE
c4201fc6d7 cleanup: unify template-lint visual + drop 3 hardcoded color literals
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>
2026-05-11 03:04:30 +02:00
xRangerDE
1123b9ac46 cleanup: extract .inline-toggle class — compact filter-row toggle pattern
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>
2026-05-11 02:58:52 +02:00
xRangerDE
ac42ec3686 cleanup: extract 17 toggle-row inline styles into one .toggle-row class
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>
2026-05-11 02:53:02 +02:00
xRangerDE
5d61094226 cleanup: extract recurring inline form patterns into utility classes
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>
2026-05-11 02:43:23 +02:00
xRangerDE
e09efd4a33 cleanup: status bar — drop stale v4.1.13 hardcode + extract inline styles
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>
2026-05-11 02:28:28 +02:00
xRangerDE
5f514b1700 i18n: localize 3 empty-state strings (VOD grid + Merge tab)
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>
2026-05-11 02:20:29 +02:00
xRangerDE
5da4cc9e64 cleanup: unify filter inputs + monospace template inputs into shared classes
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>
2026-05-11 02:09:45 +02:00
xRangerDE
ee8f9425fc cleanup: extract events/chat viewer inline styles + drop dead var
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>
2026-05-11 02:05:37 +02:00
xRangerDE
b37244cccf a11y+i18n: localize modal close aria-labels + strip dead modal title fallbacks
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>
2026-05-11 02:00:28 +02:00