diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index 8796e5b0..e80f78d8 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -734,7 +734,7 @@ jobs: curl -fsSL "${PAGES_URL}/alpha/latest.json" -o _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "stable-latest.json" --output _site/stable/latest.json || echo '{}' > _site/stable/latest.json bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html - bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/index.html + bun scripts/build-release-history-page.ts --releases-json _site/releases.json --release-notes-dir release-notes --output-file _site/index.html mkdir -p _site/download cp _site/stable/download/index.html _site/download/index.html diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a327a41..ed4d32f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -785,7 +785,7 @@ jobs: gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "alpha-latest.json" --output _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json curl -fsSL "${PAGES_URL}/stable/latest.json" -o _site/stable/latest.json || echo '{}' > _site/stable/latest.json bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html - bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/index.html + bun scripts/build-release-history-page.ts --releases-json _site/releases.json --release-notes-dir release-notes --output-file _site/index.html mkdir -p _site/download cp _site/stable/download/index.html _site/download/index.html diff --git a/release-notes/stable-v2026.5.2.md b/release-notes/stable-v2026.5.2.md new file mode 100644 index 00000000..430ef5eb --- /dev/null +++ b/release-notes/stable-v2026.5.2.md @@ -0,0 +1,18 @@ +## New Features + +- ๐Ÿ“‹ **Paste Without Formatting** โ€” Paste copied text as plain content without bringing unwanted styling into a note. +- ๐Ÿ‡ต๐Ÿ‡ฑ **Polish Language Support** โ€” Use Tolaria with a new Polish interface translation. +- ๐Ÿงญ **Refined Titlebar Navigation** โ€” Navigate with clearer titlebar controls that feel more native on desktop. + +## Improvements + +- โšก **Faster Note Loading** โ€” Switch between notes more smoothly by reusing cached note content and parsed editor blocks. +- ๐Ÿ—‚๏ธ **Cleaner Sidebar and Menus** โ€” Scan folders, sidebar sections, slash commands, icon choices, and release tabs with cleaner spacing and styling. +- ๐Ÿ–ฅ๏ธ **Better Cross-Platform Window Controls** โ€” Use macOS and Linux window controls that better match each platform. +- โ˜€๏ธ **Improved Light Mode Editing** โ€” Read code blocks more comfortably when Tolaria is using the light theme. + +## Stability and Fixes + +- This release also improves editor reliability around block dragging, table handles, pasted Markdown, stale side-menu actions, and unrelated vault refreshes. +- Vault, type, and frontmatter handling were hardened to prevent freezes, filename collisions, parse failures, and stale saved-view deletes. +- Startup, Linux AppImage, release CI, and AI/Codex integration fixes are included in the full commit list. diff --git a/scripts/build-release-history-page.ts b/scripts/build-release-history-page.ts index b014de4e..8d08755e 100644 --- a/scripts/build-release-history-page.ts +++ b/scripts/build-release-history-page.ts @@ -1,5 +1,5 @@ -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' -import { dirname, resolve } from 'node:path' +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' +import { basename, dirname, extname, resolve } from 'node:path' import { buildReleaseHistoryPage } from '../src/utils/releaseHistoryPage' @@ -14,6 +14,12 @@ function getArg(flag: string): string { return value } +function getOptionalArg(flag: string): string | null { + const index = process.argv.indexOf(flag) + const value = index >= 0 ? process.argv[index + 1] : null + return value || null +} + function readReleasePayload(filePath: string): unknown { try { return JSON.parse(readFileSync(filePath, 'utf8')) @@ -22,10 +28,28 @@ function readReleasePayload(filePath: string): unknown { } } +function readReadableReleaseNotes(directoryPath: string | null): Record { + if (directoryPath === null) return {} + + const resolvedDirectory = resolve(directoryPath) + if (!existsSync(resolvedDirectory)) return {} + + return Object.fromEntries( + readdirSync(resolvedDirectory) + .filter(fileName => extname(fileName) === '.md') + .map(fileName => { + const filePath = resolve(resolvedDirectory, fileName) + const tagName = basename(fileName, '.md') + return [tagName, readFileSync(filePath, 'utf8')] + }), + ) +} + const releasesJsonPath = resolve(getArg('--releases-json')) const outputFilePath = resolve(getArg('--output-file')) const releasesPayload = readReleasePayload(releasesJsonPath) -const html = buildReleaseHistoryPage(releasesPayload) +const readableReleaseNotes = readReadableReleaseNotes(getOptionalArg('--release-notes-dir')) +const html = buildReleaseHistoryPage(releasesPayload, readableReleaseNotes) mkdirSync(dirname(outputFilePath), { recursive: true }) writeFileSync(outputFilePath, html) diff --git a/src/utils/releaseHistoryPage.test.ts b/src/utils/releaseHistoryPage.test.ts index a69d7395..7af44907 100644 --- a/src/utils/releaseHistoryPage.test.ts +++ b/src/utils/releaseHistoryPage.test.ts @@ -47,6 +47,34 @@ describe('buildReleaseHistoryPage', () => { expect(html).toContain('Alpha notes') expect(html).toContain('Tolaria-setup.exe') expect(html).toContain('View on GitHub') + expect(html).not.toContain('class="release-channel"') + }) + + it('renders optional readable notes for stable releases instead of the commit list', () => { + const html = buildReleaseHistoryPage([ + { + body: "## What's Changed\n\n- ee71a00 feat: add paste without formatting command", + body_html: [ + '

What's Changed

', + '', + ].join(''), + html_url: 'https://github.com/refactoringhq/tolaria/releases/tag/stable-v2026.5.2', + name: 'Tolaria 2026.5.2', + prerelease: false, + published_at: '2026-05-02T16:15:00Z', + tag_name: 'stable-v2026.5.2', + }, + ], { + 'stable-v2026.5.2': '## New Features\n\n- ๐Ÿ“‹ **Paste Without Formatting** โ€” Paste copied text as plain content.', + }) + + expect(html).toContain('๐Ÿ“‹ Paste Without Formatting') + expect(html).not.toContain('data-release-detail-tab') + expect(html).not.toContain('Readable') + expect(html).not.toContain('Commits') + expect(html).not.toContain('ee71a00 feat: add paste without formatting command') }) it('falls back to escaped paragraph markup when rendered html is unavailable', () => { diff --git a/src/utils/releaseHistoryPage.ts b/src/utils/releaseHistoryPage.ts index 718a1fc7..7926f6d3 100644 --- a/src/utils/releaseHistoryPage.ts +++ b/src/utils/releaseHistoryPage.ts @@ -1,3 +1,5 @@ +import { renderReadableReleaseNotesMarkdown } from './releaseReadableNotes' + type ReleaseAssetPayload = { browser_download_url?: unknown name?: unknown @@ -28,11 +30,13 @@ type ReleaseEntry = { notesHtml: string publishedLabel: string publishedTimestamp: number + readableNotesHtml: string | null tagName: string title: string } type ReleaseSections = Record +type ReadableReleaseNotesByTag = Record const RELEASE_HISTORY_PAGE_STYLES = ` :root { @@ -117,11 +121,6 @@ const RELEASE_HISTORY_PAGE_STYLES = ` line-height: 1.6; } - .keyboard-hint { - margin-top: 8px; - font-size: 0.9375rem; - } - .channel-tabs { margin-bottom: 24px; } @@ -197,12 +196,12 @@ const RELEASE_HISTORY_PAGE_STYLES = ` flex-wrap: wrap; justify-content: space-between; gap: 12px; - margin-bottom: 14px; + margin-bottom: 26px; } .release-header h2 { margin: 0; - font-size: 1.25rem; + font-size: 1.9rem; line-height: 1.25; } @@ -212,7 +211,7 @@ const RELEASE_HISTORY_PAGE_STYLES = ` font-size: 0.9375rem; } - .release-channel { + .release-github-link { align-self: start; background: var(--release-surface-tab-hover); border-radius: 999px; @@ -221,12 +220,18 @@ const RELEASE_HISTORY_PAGE_STYLES = ` font-weight: 700; letter-spacing: 0.02em; padding: 6px 10px; + text-decoration: none; text-transform: uppercase; } - .release-card--alpha .release-channel { - background: var(--release-alpha-bg); - color: var(--release-alpha-text); + .release-github-link:hover, + .release-github-link:focus-visible { + filter: brightness(0.96); + } + + .release-github-link:focus-visible { + outline: 2px solid var(--release-accent); + outline-offset: 2px; } .release-notes { @@ -250,6 +255,18 @@ const RELEASE_HISTORY_PAGE_STYLES = ` margin: 1.2em 0 0.4em; } + .release-notes h1 { + font-size: 1.35rem; + } + + .release-notes h2 { + font-size: 1.25rem; + } + + .release-notes h3 { + font-size: 1.1rem; + } + .release-notes p, .release-notes ul, .release-notes ol, @@ -409,6 +426,7 @@ const RELEASE_HISTORY_PAGE_SCRIPT = ` tab.addEventListener('click', () => activateTab(tab)); tab.addEventListener('keydown', (event) => handleTabKeydown(event, tab, index)); }); + })(); ` @@ -528,12 +546,35 @@ function normalizeReleaseEntry(release: GitHubReleasePayload): [ReleaseChannel, notesHtml: resolveReleaseNotesHtml(release.body_html, release.body), publishedLabel: formatPublishedLabel(release.published_at), publishedTimestamp: parsePublishedTimestamp(release.published_at), + readableNotesHtml: null, tagName, title, }] } -function collectReleaseSections(payload: unknown): ReleaseSections { +function applyReadableReleaseNotes(release: ReleaseEntry, readableReleaseNotesByTag: ReadableReleaseNotesByTag): ReleaseEntry { + const readableNotesMarkdown = normalizeText(readableReleaseNotesByTag[release.tagName]) + if (readableNotesMarkdown === null) return release + + return { + ...release, + readableNotesHtml: renderReadableReleaseNotesMarkdown({ markdown: readableNotesMarkdown }), + } +} + +function appendReleaseSection( + sections: ReleaseSections, + normalizedRelease: [ReleaseChannel, ReleaseEntry], + readableReleaseNotesByTag: ReadableReleaseNotesByTag, +): void { + const [channel, release] = normalizedRelease + const releaseEntry = channel === 'stable' + ? applyReadableReleaseNotes(release, readableReleaseNotesByTag) + : release + sections[channel].push(releaseEntry) +} + +function collectReleaseSections(payload: unknown, readableReleaseNotesByTag: ReadableReleaseNotesByTag): ReleaseSections { const sections: ReleaseSections = { alpha: [], stable: [] } if (!Array.isArray(payload)) return sections @@ -543,8 +584,7 @@ function collectReleaseSections(payload: unknown): ReleaseSections { const normalizedRelease = normalizeReleaseEntry(item as GitHubReleasePayload) if (normalizedRelease === null) continue - const [channel, release] = normalizedRelease - sections[channel].push(release) + appendReleaseSection(sections, normalizedRelease, readableReleaseNotesByTag) } for (const channel of ['stable', 'alpha'] as const) { @@ -573,17 +613,15 @@ function buildTabMarkup(channel: ReleaseChannel, count: number, selected: boolea function buildReleaseMarkup(channel: ReleaseChannel, release: ReleaseEntry): string { const downloads = [...release.downloads] - if (release.githubUrl !== null) { - downloads.push({ label: 'View on GitHub', url: release.githubUrl }) - } - const channelLabel = RELEASE_CHANNEL_LABELS[channel] + const githubMarkup = release.githubUrl === null + ? '' + : `View on GitHub` const downloadsMarkup = downloads.length > 0 ? `
${downloads.map(download => { - const isSecondary = download.label === 'View on GitHub' - return `${escapeHtml(download.label)}` + return `${escapeHtml(download.label)}` }).join('')}
` : '' @@ -595,9 +633,9 @@ function buildReleaseMarkup(channel: ReleaseChannel, release: ReleaseEntry): str

${escapeHtml(release.title)}

${escapeHtml(release.publishedLabel)} ยท ${escapeHtml(release.tagName)}

- ${channelLabel} + ${githubMarkup} -
${release.notesHtml}
${downloadsMarkup} +
${release.readableNotesHtml ?? release.notesHtml}
${downloadsMarkup} ` } @@ -619,8 +657,11 @@ function buildPanelMarkup(channel: ReleaseChannel, releases: ReleaseEntry[], sel ` } -export function buildReleaseHistoryPage(releasesPayload: unknown): string { - const sections = collectReleaseSections(releasesPayload) +export function buildReleaseHistoryPage( + releasesPayload: unknown, + readableReleaseNotesByTag: ReadableReleaseNotesByTag = {}, +): string { + const sections = collectReleaseSections(releasesPayload, readableReleaseNotesByTag) return ` @@ -636,7 +677,6 @@ export function buildReleaseHistoryPage(releasesPayload: unknown): string {

Tolaria Release History

Stable builds appear when a stable-vYYYY.M.D tag is promoted. Alpha builds update on every push to main.

-

Use Tab to reach the channel picker, then use the arrow keys to switch between Stable and Alpha.

diff --git a/src/utils/releaseReadableNotes.ts b/src/utils/releaseReadableNotes.ts new file mode 100644 index 00000000..3151bdb0 --- /dev/null +++ b/src/utils/releaseReadableNotes.ts @@ -0,0 +1,65 @@ +type ReadableMarkdownSource = { + markdown: string +} + +type ReadableMarkdownLine = + | { kind: 'blank' } + | { html: string, kind: 'bullet' | 'heading' | 'paragraph' } + +function escapeHtml(source: ReadableMarkdownSource): string { + return source.markdown + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') +} + +function renderInlineMarkdown(source: ReadableMarkdownSource): string { + return escapeHtml(source) + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/`([^`]+)`/g, '$1') +} + +function renderMarkdownLine(source: ReadableMarkdownSource): ReadableMarkdownLine { + const line = source.markdown.trim() + if (line.length === 0) return { kind: 'blank' } + + const heading = line.match(/^(#{1,3})\s+(.+)$/) + if (heading) { + const level = heading[1].length + return { html: `${renderInlineMarkdown({ markdown: heading[2] })}`, kind: 'heading' } + } + + const bullet = line.match(/^[-*]\s+(.+)$/) + if (bullet) return { html: `
  • ${renderInlineMarkdown({ markdown: bullet[1] })}
  • `, kind: 'bullet' } + + return { html: `

    ${renderInlineMarkdown({ markdown: line })}

    `, kind: 'paragraph' } +} + +function appendRenderedLine(html: string[], line: ReadableMarkdownLine, listOpen: boolean): boolean { + if (line.kind === 'blank') { + if (listOpen) html.push('') + return false + } + if (line.kind === 'bullet') { + if (!listOpen) html.push('
      ') + html.push(line.html) + return true + } + + if (listOpen) html.push('
    ') + html.push(line.html) + return false +} + +export function renderReadableReleaseNotesMarkdown(source: ReadableMarkdownSource): string { + const html: string[] = [] + let listOpen = false + + for (const rawLine of source.markdown.split('\n')) { + listOpen = appendRenderedLine(html, renderMarkdownLine({ markdown: rawLine }), listOpen) + } + + if (listOpen) html.push('') + return html.join('') +}