Files
psysonic/src/utils/changelogMarkdown.tsx
T
Psychotoxical 6bd2bfc01c feat(changelog): replace auto-modal with sidebar banner + /whats-new page
Removes the giant startup changelog modal. After an update, a compact
neutral-palette pill now sits above Now Playing in the sidebar saying
"Changelog — vX.Y.Z". Clicking opens a proper /whats-new page in the
main content area; X dismisses. Page renders the current version's
CHANGELOG entry with inline Markdown (headings, lists, blockquotes,
hr, bold/italic/code, and real [label](url) links that open via the
Tauri shell plugin).

Visual tokens are hardcoded slate/gray/blue so the banner and page look
identical across every theme (dark, light, skeuomorphic, whatever) —
requested for consistent contrast. Auto-mark-as-seen removed from the
page; the banner only goes away on explicit dismiss, so users can
re-read as often as they want.

Settings → About gains a "Release notes" row with a link that resets
the seen-version and opens the page, so the banner can be retriggered
manually (also helpful for dev builds ahead of the current tag).

The existing "Show changelog on update" toggle now gates the banner
instead of the modal; description strings updated in all 8 locales.

fix(themes): WCAG contrast audit — mocha + winmedplayer + wista

Mocha (Catppuccin dark)
  .track-size used --ctp-overlay0 directly (3.36:1 on bg-app, 2.57
  on bg-card). Swapped to --text-muted → 7.37 / 5.65. Fix also lifts
  macchiato/frappe/latte which shared the failure.

WinMedPlayer (Luna)
  --text-muted #b8d0f8 (3.87:1 on bg-app) → #e8f0ff (5.28)
  --border   #2a5090 (1.31 on bg-app)   → #071027 (3.12, meets 3:1 UI)

Wista (Vista Aero)
  Lyrics pane sits on bg-sidebar #0e1e3e but used --text-primary
  (#0d1d3c) for active lines — 1.01:1, literally invisible. Added
  component overrides: .lyrics-line / .lyrics-status / word-synced
  variants → #aac8f0 (9.60), .lyrics-line.active → #ffffff (16.48).
  Palette: --warning #c8980c → #735a00 (2.37 → 5.91 on bg-app),
  --text-muted #4870a8 → #3f6aa0 (4.23 → 4.65 on bg-card).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 02:28:45 +02:00

122 lines
4.0 KiB
TypeScript

import React from 'react';
import { open } from '@tauri-apps/plugin-shell';
/**
* Render inline markdown segments: **bold**, *italic*, `code`, [text](url).
* External links open in the user's default browser via the Tauri shell plugin.
*/
export function renderInlineMarkdown(text: string, keyPrefix = 'i'): React.ReactNode[] {
// Tokenize — order matters: links first (no recursion), then emphasis/code.
const tokens: React.ReactNode[] = [];
const linkRe = /\[([^\]]+)\]\(([^)]+)\)/g;
let lastIndex = 0;
let match: RegExpExecArray | null;
let i = 0;
const pushInline = (segment: string) => {
const parts = segment.split(/(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g);
for (const part of parts) {
if (!part) continue;
if (part.startsWith('**') && part.endsWith('**')) {
tokens.push(<strong key={`${keyPrefix}-${i++}`}>{part.slice(2, -2)}</strong>);
} else if (part.startsWith('*') && part.endsWith('*') && part.length > 2) {
tokens.push(<em key={`${keyPrefix}-${i++}`}>{part.slice(1, -1)}</em>);
} else if (part.startsWith('`') && part.endsWith('`')) {
tokens.push(<code key={`${keyPrefix}-${i++}`} className="whats-new-code">{part.slice(1, -1)}</code>);
} else {
tokens.push(part);
}
}
};
while ((match = linkRe.exec(text)) !== null) {
if (match.index > lastIndex) pushInline(text.slice(lastIndex, match.index));
const [full, label, url] = match;
tokens.push(
<a
key={`${keyPrefix}-link-${i++}`}
href={url}
onClick={(e) => { e.preventDefault(); open(url).catch(() => {}); }}
className="whats-new-link"
>
{label}
</a>
);
lastIndex = match.index + full.length;
}
if (lastIndex < text.length) pushInline(text.slice(lastIndex));
return tokens;
}
/**
* Render a subset of GitHub-flavored Markdown used by our CHANGELOG: headings
* (### / ####), bullets (- / *), blockquotes, horizontal rules, and inline
* formatting (bold/italic/code/links).
*/
export function renderChangelogBody(body: string): React.ReactNode[] {
const lines = body.split('\n');
const out: React.ReactNode[] = [];
let bulletBuffer: React.ReactNode[] = [];
let quoteBuffer: string[] = [];
const flushBullets = () => {
if (bulletBuffer.length === 0) return;
out.push(<ul key={`ul-${out.length}`} className="whats-new-list">{bulletBuffer}</ul>);
bulletBuffer = [];
};
const flushQuote = () => {
if (quoteBuffer.length === 0) return;
out.push(
<blockquote key={`q-${out.length}`} className="whats-new-quote">
{renderInlineMarkdown(quoteBuffer.join(' '), `q-${out.length}`)}
</blockquote>
);
quoteBuffer = [];
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
if (trimmed === '') { flushBullets(); flushQuote(); continue; }
if (trimmed === '---') {
flushBullets(); flushQuote();
out.push(<hr key={`hr-${out.length}`} className="whats-new-hr" />);
continue;
}
if (line.startsWith('### ')) {
flushBullets(); flushQuote();
out.push(<h3 key={`h3-${out.length}`} className="whats-new-h3">{renderInlineMarkdown(line.slice(4), `h3-${i}`)}</h3>);
continue;
}
if (line.startsWith('#### ')) {
flushBullets(); flushQuote();
out.push(<h4 key={`h4-${out.length}`} className="whats-new-h4">{renderInlineMarkdown(line.slice(5), `h4-${i}`)}</h4>);
continue;
}
if (line.startsWith('> ')) {
flushBullets();
quoteBuffer.push(line.slice(2));
continue;
}
if (line.startsWith('- ') || line.startsWith('* ')) {
flushQuote();
bulletBuffer.push(
<li key={`li-${i}`}>{renderInlineMarkdown(line.slice(2), `li-${i}`)}</li>
);
continue;
}
// Paragraph / plain line
flushBullets(); flushQuote();
out.push(<p key={`p-${i}`} className="whats-new-p">{renderInlineMarkdown(line, `p-${i}`)}</p>);
}
flushBullets(); flushQuote();
return out;
}