Commit Graph

183 Commits

Author SHA1 Message Date
xRangerDE
7a6654097f i18n: localize 3 placeholders that stayed in German under English
Three input placeholders in the HTML had hardcoded German strings that were never routed through the locale system:

- clipStartPart (Clip cutter modal — "Start Part-Nummer") said "z.B. 42" regardless of language
- clipUrl (Clips tab) read "...oder https://www.twitch.tv/..." with a mid-string German conjunction
- cutterFilePath (Video cutter "Datei ausgewahlt" field) read "Keine Datei ausgewahlt..." even under EN

Added three locale keys (clips.urlPlaceholder, clips.startPartPlaceholder, cutter.filePathPlaceholder) with DE+EN translations, plus the three setPlaceholder() wire-up calls in renderer-texts.ts. The HTML defaults stay German (consistent with the other placeholders) but the JS now overrides them when the user picks English (or re-renders when language changes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:54:08 +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
f6f266e3d4 a11y: localized aria-label + focus-visible on the 3 merge file-row buttons
The merge tab's per-file action buttons (▲ ▼ x — move up, move down, remove from list) were three icon-only buttons whose only visible content was the unicode glyph. No aria-label, no title, no focus-visible ring:

- Screen readers had nothing to announce — a keyboard user navigating the merge file list would tab through three unnamed buttons in a row.
- Sighted keyboard users had no visible focus indicator on .file-btn.

Three new locale keys (DE+EN) — merge.moveUpAria / moveDownAria / removeAria — give each button a translated aria-label and matching title tooltip. CSS adds .file-btn:focus-visible with the purple ring and a red variant for .file-btn.remove to match its red-on-hover colour family.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:42:10 +02:00
xRangerDE
bcc7eea968 defensive: type="button" on the 16 renderer-rendered <button> template strings
Continuing the type-attribute pass from 4.6.114 (static HTML). The renderer modules build buttons into their template strings via tagged-template literals; sixteen of these were rendered without an explicit type attribute and would inherit the type="submit" default:

- renderer-archive.ts: 4 archive-result buttons (chat / events viewer triggers + open file / show in folder)
- renderer-profile.ts: 3 profile action buttons (live record CTA, open on Twitch, refresh)
- renderer-queue.ts: 4 queue detail-row buttons (open file, show in folder, view chat, view events)
- renderer-streamers.ts: 2 per-VOD-card buttons (trim, queue)
- renderer.ts: 3 merge file row buttons (move up, move down, remove)

Same defensive reasoning as 4.6.114: no <form> wraps these today, but the moment one does they all silently turn into submit buttons. Explicit type="button" closes that footgun. Every <button> in the codebase — static or dynamic — now declares its type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:36:31 +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
85128086b4 a11y: focus-visible on .streamer-profile-btn + type=button on storage open btn
Two small targeted fixes:

- .streamer-profile-btn (the action buttons in the streamer profile header: Record now, Open on Twitch, Refresh) had :hover but no :focus-visible. Keyboard users tabbing through the profile header buttons couldn't tell which one was focused. Added a purple ring for the default variant and the inner-white + outer-purple double-ring for the .primary variant (matches the convention used everywhere else for purple-background buttons).

- The dynamically-built Storage table "Open" button (renderer-settings.ts:397) was created via document.createElement('button') without setting type. Browsers default to type="submit" for <button> elements, which is fine outside a form but defensive coding to mark it explicitly. Set openBtn.type = 'button'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:27:38 +02:00
xRangerDE
1da5589b1a a11y: aria-label on queue retry button + streamer live-dot
Two icon-only elements that had a title tooltip but no programmatic accessible-name source for screen readers:

- .queue-retry-btn: a small button on failed queue items, its only visible content is the unicode glyph U+21BB (↻). Screen readers would read it as "clockwise open circle arrow" or skip it entirely. Added aria-label using the already-translated UI_TEXT.queue.retryItem string (same value as the title). Also added type="button" so it doesn't default to submit semantics if the queue is ever wrapped in a form.

- .streamer-live-dot: the small red dot that appears next to a streamer name when they're currently live. It's pure CSS styling on an empty span, so screen readers had nothing to read — losing the live state for assistive tech. Added role="img" + aria-label using the existing UI_TEXT.streamers.liveNowTooltip string, so screen readers announce "Live now" alongside the streamer name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:23:21 +02:00
xRangerDE
632f348349 a11y: aria-hidden on the 6 decorative SVG icons rendered from TS
Continuing the SVG aria-hidden pass from 4.6.110 (which covered index.html). The renderer modules build six more decorative SVG icons into their template strings:

- renderer-profile.ts: followers icon, vods icon, last-stream clock, live-viewers eye
- renderer-queue.ts: merge-group icon next to merge-bundled queue rows
- renderer-streamers.ts: empty-state VOD icon shown when a streamer has no VODs

All 6 are pure decoration — each sits next to a textual label or value (e.g. "1.2K followers", "5 VODs", a viewer count, etc.). Screen readers were either announcing them as "image/graphic" or silently traversing — adding aria-hidden="true" cleanly skips them so the assistive-tech read-out is just the meaningful text.

The data-URL SVG fallback in renderer-streamers.ts:257 (used as an onerror src for VOD thumbnails) is intentionally not touched — it's an image fallback inside a URL-encoded string, not an actual SVG element in the DOM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:17:05 +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
43924fd51f a11y: aria-hidden on three decorative middle-dot separators
Three places in the renderer use a literal middle-dot character (· / &middot;) as a visual separator between two label components:

- Sidebar streamer-section counter: "12 · 3 live"
- Stats top-streamers row sub-label: "streamerName · 5 files"
- Stats size-bucket row sub-label: "count · size"

Screen readers either announce "middle dot" verbatim or skip it depending on the implementation — neither is useful information. Wrapped each dot in <span aria-hidden="true"> so the assistive-tech read-out becomes "12 3 live" / "streamerName 5 files" / "count size" — the meaningful parts only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:08:08 +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
03b575cd1d a11y: scope=col + aria-label on storage stats table headers
The Storage card's per-streamer stats table was built without scope attributes on its <th> cells and the last column (the "Open" action column) had an empty header — screen readers couldn't tell which column a data cell belonged to once you tabbed into the rows, and the actions column had no announced name at all.

Fixes:
- All 6 column headers get scope="col" — explicit signal to screen readers that each <th> is a column header (the implicit <th>-inside-<thead> semantics work, but explicit scope is best practice for data tables and the de-facto standard for accessibility tooling validation)
- The empty actions header gets aria-label set from a new storageColumnActionsAria locale key (DE: "Aktionen", EN: "Actions") so screen readers announce "Aktionen, Spaltenkopf" for that column instead of skipping over an unnamed header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:57:33 +02:00
xRangerDE
7dd6755392 a11y: respect prefers-reduced-motion — suppress all animations/transitions
The app had no @media (prefers-reduced-motion: reduce) rule, meaning users with the OS-level "Reduce motion" setting enabled still got the full animation set: the empty-state floating SVG (4s infinite), the btn-icon-spin on Refresh hover, the vod-bulk-bar slide-in, the storyboard fade-in, and the ~6 transition: all declarations scattered across button hover states.

For users with vestibular disorders this is real discomfort, not aesthetic preference. Windows 11 and macOS both expose the setting via Settings > Accessibility, and the media query is the standard way to honour it from CSS.

Added the conventional reduce-motion block at the bottom of styles.css:
- animation-duration: 0.01ms (effectively instant)
- animation-iteration-count: 1 (kills infinite loops)
- transition-duration: 0.01ms (state changes are immediate)
- scroll-behavior: auto (kills smooth-scroll)

All hover/state changes still happen — they just snap rather than animate. No feature is lost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:51:23 +02:00
xRangerDE
5a4b054d9d a11y: focus-visible rings on .vod-btn (per-card action buttons)
Each VOD card renders 2-3 action buttons in its footer (.vod-btn — Trim, Queue, etc.). They had :hover states but no :focus-visible, so keyboard users tabbing through the VOD grid would land on each button without any visual focus indicator.

Added:
- .vod-btn:focus-visible — purple ring (covers the secondary variant which has a translucent grey bg)
- .vod-btn.primary:focus-visible — inner-white + outer-purple double-ring so the indicator stays visible against the button's own purple background (same pattern used for .btn-pill.primary and .btn-primary in 4.6.81/82)

Continues the per-area focus-visible pass — VOD-grid keyboard nav (already covered: card itself, checkbox; now: action buttons inside the card) is the final big interactive surface to get coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:46:32 +02:00
xRangerDE
9be864e614 a11y: focus-visible on .modal-close + .queue-retry-btn
Two more interactive buttons that had hover + active states but no keyboard focus indicator:

- .modal-close (the X-close button used by all 5 modals — update, clip-cutter, events viewer, chat viewer, template guide). Red-toned ring matching the hover colour family.

- .queue-retry-btn (per-item retry button on failed queue items). Purple ring matching the hover state's accent-purple feedback.

Continues the focus-visible pass started in 4.6.81/82/103. Closes the remaining shell-and-modal buttons that keyboard users could tab to without seeing where they were.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:42:21 +02:00
xRangerDE
91e4e65fa6 a11y: focus-visible ring on .lang-option (language picker)
The two language-picker buttons (Deutsch / English) had :hover and an
.active state but no :focus-visible — keyboard users tabbing into the
group couldn't see which button was focused unless it also happened to
be the active one. And even then, the active state uses a 1px soft
shadow which is easy to mistake for the hover border tweak.

Added two rules:
- .lang-option:focus-visible — purple-accent ring matching the rest of
  the app's focus-visible convention
- .lang-option.active:focus-visible — combines the pressed-state border
  with the thicker 2px focus halo so focus and pressed state are both
  visible when they coincide

Continues the focus-visible pass started in 4.6.81 (btn-primary/secondary/
pill/close) and 4.6.82 (queue + top-bar + add-streamer buttons).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:37:37 +02:00
xRangerDE
752a5b2556 cleanup: centralise localStorage try/catch in renderer-shared — safeLocalStorageGet/Set/Remove
Every renderer module that persists state was wrapping its localStorage.getItem/setItem/removeItem call in the same try/catch idiom — handling private-browsing quirks and other sandbox contexts where storage isn't writable. Three identical patterns repeated nine times across renderer-streamers (filter / sort / hide-downloaded state), renderer-updates (skipped-update version), and renderer.ts (active-tab persistence).

Introduced three helpers in renderer-shared.ts:
- safeLocalStorageGet(key, fallback = '') — wraps getItem with the try/catch + fallback
- safeLocalStorageSet(key, value) — wraps setItem
- safeLocalStorageRemove(key) — wraps removeItem (needed for clearSkippedUpdateVersion which actually deletes the entry rather than blanking it)

Refactored 9 callsites. Reduces the noise:before:
    try { return localStorage.getItem(KEY) ?? ''; } catch { return ''; }
    try { localStorage.setItem(KEY, value); } catch { /* localStorage may be unavailable */ }
after:
    return safeLocalStorageGet(KEY);
    safeLocalStorageSet(KEY, value);

Left the VOD scroll-positions persistence in renderer-streamers untouched — its surrounding try/catch wraps JSON.parse/stringify logic that doesn't fit the simple helper signature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:33:23 +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
a7f16d8cf8 observability: auto-updater lifecycle events go through appendDebugLog, not console.log
Four autoUpdater event handlers (checking-for-update, update-available, update-not-available, update-downloaded) were logging via raw console.log while the sibling 'error' handler already used appendDebugLog. Two consequences:

1. In a packaged build the user has no visible record of the update lifecycle — console.log streams to stderr which is invisible without DevTools. appendDebugLog writes to the timestamped debug log file that the user can inspect via the Live Debug-Log card in Settings.

2. Inconsistent — the existing 'auto-updater-error' tag in line 6479 was the only update-related event reaching the debug log. New tags ('auto-updater-checking', 'auto-updater-update-available', 'auto-updater-update-not-available', 'auto-updater-update-downloaded') give the full lifecycle a coherent grep-friendly prefix in the log.

The version info that was being printed inline ("Update available: 4.7.0") now lives in the structured details payload instead of a free-form message — easier to parse mechanically and matches the rest of the codebase's debug-log conventions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:21:35 +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
f29cfd6ed4 a11y: focus-visible rings on queue-action + top-bar + add-streamer buttons
Continuing the keyboard-focus pass from 4.6.81. Five more interactive controls in the always-visible UI surface had no visible focus indicator:

- .btn (the shared base for queue-action buttons — Start/Merge/Wiederholen/Leeren)
- .btn-start (green): inner-white + green-outer ring so it stays visible against the success-green background, and a red-outer variant when .downloading is active (button switches to error red)
- .btn-icon (top-bar Refresh)
- .header-search button (the purple "+" add-streamer button next to the streamer input — uses inner-white + accent-outer like .btn-pill.primary so the ring stays visible against the purple bg)
- .header-search input (was bare, now matches the .btn-icon focus convention — purple border + soft halo)

Tab order on the main shell is now fully keyboard-traversable with visible state at every step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:53:03 +02:00
xRangerDE
66486dba0c a11y: focus-visible rings on primary action buttons + missing hover on .btn-secondary
Keyboard-only users had no visible focus indicator on five widely-used button classes:
- .btn-primary (main action button — used in clip download, modal confirms)
- .btn-secondary (cancel buttons, neutral actions; also missing hover transitions)
- .btn-pill (toolbar/bulk-bar action buttons; primary + success + danger variants)
- .btn-close (X-close button used in filter clears, inline removals)
- .queue-detail-btn (queue item detail chip buttons + archive companion buttons)

Tabbing through these buttons left no indication of which one would activate on Enter/Space — WCAG 2.4.7 (Focus Visible) violation.

Added :focus-visible rings using the established box-shadow convention (purple-accent for default, white-inner + accent-outer for purple/green pill variants so the ring stays visible against the button's own purple background, red-toned for .btn-close / .btn-pill.danger). Also added :hover + transition to .btn-secondary which previously had neither — clicking felt unresponsive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:48:36 +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
e705beabf3 cleanup: dedupe setPageTitle fallback in renderer-settings
4.6.77 inlined the setPageTitle global-window resolution + fallback
twice — once in each branch of the "vods+streamer vs other tab"
conditional. Reading two near-identical 4-line if/else stanzas back
to back was harder than necessary.

Collapsed the branch to a single ternary that picks the title text
first, then a single setPageTitle resolve + apply block at the end.
Same behavior, one resolution of the optional global.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:33:47 +02:00
xRangerDE
bdf6bac602 feat: window title syncs with active tab / streamer
document.title was stamped once during app boot with the static
"Twitch VOD Manager vX.Y.Z" string. After that, the H1 page-title
in the header updated as the user navigated tabs and selected
streamers, but the OS-level window title — the string shown in the
taskbar, Alt+Tab switcher, and OS notifications — never changed.

Multitasking suffered: a user with three Electron windows pinned
to taskbar all read identical "Twitch VOD Manager v4.6.x", with
no clue which window had what tab or streamer loaded.

Added a setPageTitle(text) helper in renderer.ts that:
- Updates the H1 #pageTitle textContent (the visible header)
- Updates document.title with `${text} - ${appName} v${version}`
  for non-default text, or just `${appName} v${version}` for the
  default app-name fallback
- Exposed on window so the renderer-streamers.ts and
  renderer-settings.ts modules can reach it without crossing the
  module-vs-bundle boundary

Three call sites updated to use the helper:
- showTab → uses for tab-derived titles
- selectStreamer → uses for "xrohat" style streamer titles
- the renderer-settings language-switch refresh path

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:29:04 +02:00
xRangerDE
69b83c9d22 cleanup+a11y: VOD select checkbox — proper aria-label + CSS-class styling
The bulk-select checkbox on each VOD card was carrying a ~140 char
inline-style block (absolute positioning, dimensions, accent-color,
z-index, cursor) — duplicated across every rendered VOD — plus a
truly bizarre title-attribute fallback that hacked the
bulkSelectedCount locale string by stripping placeholder digits:

  title=`${UI_TEXT.vods.bulkSelectedCount
                .replace("{count}", "0")
                .replace(/[0-9]/g, "")
                .trim() || "Select"}`

That worked by accident — the placeholder happens to be "{count}
ausgewahlt" / "{count} selected" so stripping digits gave a usable
fragment — but it was fragile and not really an accessible label.

Three fixes:
- Extracted the inline styles to a .vod-select-checkbox CSS rule.
  The custom checkbox styling from 4.6.26 means accent-color was
  redundant anyway, so dropping it is a no-op visually.
- Added a proper locale key vods.selectAriaLabel ("Select VOD for
  bulk action" / "VOD fuer Bulk-Aktion auswaehlen") for the
  aria-label attribute.
- Dropped the title-attribute hack entirely. aria-label now provides
  the AT-readable name; sighted users get the visual checkbox
  which is self-explanatory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:24:50 +02:00
xRangerDE
d9593091a5 feat: Ctrl+F also focuses archive search when on Archiv tab
Ctrl+F was wired to focus the VOD filter input — but only when the
VODs tab was active. On the Archive tab (added in 4.6.15) Ctrl+F
did nothing useful: the browser default find bar was suppressed
(Electron renderer doesn't have one anyway) and the app handler
didn't have a branch for the archive context.

Now Ctrl+F also targets the archiveSearchQuery input when the
Archive tab is the active tab. Other tabs (Clips / Cutter / Merge /
Stats / Settings) let the shortcut fall through to no-op since
they don't have a primary search/filter input.

Same input-focus convention as the VODs tab: focus + select-all so
the user can immediately type to replace or append.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:18:37 +02:00
xRangerDE
dba6e872a9 fix: TAB_IDS missing stats + archive — keyboard shortcuts + persistence broken
When the Statistik (4.6.14) and Archiv (4.6.15) tabs were added to
the sidebar nav, the TAB_IDS const never got extended past its
original five entries:

  const TAB_IDS = ["vods", "clips", "cutter", "merge", "settings"]

Two consequences:

1) Ctrl+1..5 keyboard shortcut was hard-capped at five tabs (the
   guard `tabIndex < TAB_IDS.length` filtered Ctrl+6 and Ctrl+7 out).
   Even though there were 7 visible tabs.

2) persistActiveTab(tab) called isKnownTab(tab) before localStorage
   write. For 'stats' or 'archive' that returned false, so the tab
   was silently NOT persisted. Open the app on the Archiv tab,
   close it, reopen — it'd boot on VODs because the persisted value
   was the previous non-stats/archive selection.

Extended TAB_IDS to all seven nav-items + bumped the keyboard
shortcut range from 1-5 to 1-7. Ctrl+5 now maps to Statistik,
Ctrl+6 to Archiv, Ctrl+7 to Einstellungen. Persistence works for
the full set.

Added a comment to TAB_IDS pointing out the failure modes when
this is out of sync with the HTML nav, so the next nav addition
doesn't repeat the bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:15:30 +02:00
xRangerDE
7e7be1d103 perf: remove 3 high-volume console.log calls in download / update paths
Three console.log calls in main.ts were flooding stdout during normal
operation:

1) `console.log("Starting download:", cmd, args)` — redundant with
   the appendDebugLog("download-part-start", ...) one line below.
   Duplicate logging; pure noise.

2) `console.log("Streamlink:", line)` — fired for every line of
   streamlink stdout, which is 10-100 lines/sec during an active
   download. Hundreds of thousands of lines per multi-hour recording.
   Progress + state parsing already happens on the same line; the
   raw output was never consumed.

3) `console.log("Download progress: X%")` in the autoUpdater
   handler — fires ~10x/sec during an in-flight update download.
   The renderer banner is the user-visible feedback; this was
   developer-only and never necessary in prod.

Removed all three. The remaining four console.log calls (login
flow, update-available, update-downloaded, no-updates-available)
are once-per-event and fine to keep.

Practical benefit: stdout becomes useful for actual diagnostics
again. Performance gain is marginal in absolute terms but the
buffered noise on a long-running session was real.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:09:29 +02:00
xRangerDE
885cbaa894 cleanup: stats size-bucket histogram — extract inline styles
Final piece of the renderer-stats.ts extraction. The recording-size
distribution histogram (6 buckets: <100MB ... >10GB) was rendering
each bucket-row as a 5-inline-style template — same shape as the
top-streamers list (margin row, flex meta header, two spans, bar
track, bar fill).

Extracted to a .stats-bucket-* family in styles.css:
- .stats-bucket-row + .stats-bucket-row:last-child margin trim
- .stats-bucket-meta + .stats-bucket-meta-sub for the flex label/
  count header
- .stats-bucket-bar-track + .stats-bucket-bar-fill for the
  horizontal bar (with width-transition so the bar fills
  animate on data refresh)

That completes the Statistik tab pass — 26 inline styles -> 22
CSS class assignments + 4 truly-dynamic width/height percent
overrides for the bar fills. Tabular numerics, hover states, and
data refresh animations all flow from the central stylesheet now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 05:04:07 +02:00
xRangerDE
c9a5223eb6 cleanup: stats activity chart — extract 30-day bar inline styles
Continuing the renderer-stats.ts inline-style extraction. The
"Aktivitaet (letzte 30 Tage)" bar chart built each day-column as
a 5-inline-style template:

  <div style="flex: 1; display:flex; flex-direction:column;
              align-items:center; gap:4px; min-width:0;">
    <div style="width: 100%; height: 90px; display:flex;
                align-items: flex-end;">
      <div style="width:100%; height: 70%;
                  background: var(--accent, #9146ff);
                  border-radius: 2px 2px 0 0;" title="...">
    <div style="font-size: 9px; color: var(--text-secondary);
                white-space: nowrap;">

30 columns rendered per refresh meant ~7.5KB of duplicated inline
style attribute strings in the DOM after every refresh.

Extracted to .stats-day-col + .stats-day-bar-track + .stats-day-bar-
fill + .stats-day-label, plus .stats-activity-row + .stats-activity-
summary for the outer wrappers. Only the per-day height percent
stays inline (it's truly dynamic, per-day data).

Polish riders:
- Bar fill picks up height: 0.3s ease-out transition so the bars
  animate up on data refresh instead of snapping
- Hover state shifts the bar from accent to accent-hover so the
  hovered day reads as the focus
- Day-label spans get tabular-nums so the "05-12" type strings
  align column-to-column

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:59:33 +02:00
xRangerDE
abc983c035 cleanup: stats top-streamers bar list — extract inline styles
Second pass on the Statistik tab. The top-10 streamers-by-size
list rendered each row as a 6-inline-style template (margin,
two flex containers, two span colour overrides, two bar wrappers,
two bar fills with hard-coded gradient).

Extracted to a .stats-top-* family in styles.css:
- .stats-top-row — outer row spacing
- .stats-top-meta + .stats-top-meta-sub for the label/byte-size
  flex header
- .stats-top-share for the muted (X.Y%) suffix
- .stats-top-bar-track + .stats-top-bar-fill for the gradient
  progress bar (now with a width-transition for the streamer-by-
  streamer animation when the data refreshes)
- .stats-top-bar-labels for the overlaid LIVE/VOD breakdown that
  gets pointer-events: none so the bar isn't accidentally hover-
  blocked

Also picked up the "no top streamers" empty-state message and
swapped its inline-style div for the existing .form-note utility
class introduced in 4.6.42.

Top streamers row hover state intentionally NOT added — these are
read-only summary rows, not interactive ones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:54:56 +02:00
xRangerDE
874f64c1ba cleanup: stats KPI cards — extract 4 inline styles into reusable classes
The six-tile overview grid at the top of the Statistik tab built
each KPI card as a four-property inline-styled div:

  <div style="background: var(--bg-elevated); border: 1px solid
              var(--border-soft); border-radius: 6px; padding: 12px;">
    <div style="font-size: 11px; color: var(--text-secondary);
                text-transform: uppercase; letter-spacing: 0.5px;">
    <div style="font-size: 22px; font-weight: 600; margin-top: 4px;">
    <div style="font-size: 12px; color: var(--text-secondary);
                margin-top: 4px;">

Each card repeated the same ~250 chars of inline styling. Card hover
state, number alignment, future polish all required editing the
renderer.

Extracted to .stats-kpi-card + .stats-kpi-label + .stats-kpi-value
+ .stats-kpi-sub. Added two enhancements while at it:
- subtle hover state (purple-tint border + 1px lift) so the cards
  feel interactive in line with the rest of the apps language
- font-variant-numeric: tabular-nums on values + subs so the
  numbers align properly across the six-tile grid

Also stats-no-root for the "Download folder not found" fallback
that grid-column-spans across all 6 columns.

The remaining 22 inline styles in renderer-stats (top-streamers
bar list, activity calendar, size buckets) come in subsequent
iterations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:50:23 +02:00