Compare commits

...

2 Commits

Author SHA1 Message Date
Maurice 48befc210d fix(journey): the share link's copy button and its switches show their words again
The filled buttons set their label in text-accent-on, the accent used as
text on a plain surface, which on the accent's own ground is the same
colour: a black button with black words. They take text-accent-text, the
colour made for words on the accent, and a test pins it.
2026-10-24 17:31:50 +02:00
Maurice 0eb5292ca4 feat(admin): release notes show a #### heading as the feature it names
The release notes in Admin read ## as the version and ### as a section,
and printed a #### line as plain text. A release that heads each feature
inside a section with #### (Tours, TREK Studio under Added) now shows it
as a feature heading between the section and its text, on desktop and on
the phone.
2026-10-24 17:31:49 +02:00
6 changed files with 37 additions and 9 deletions
@@ -102,6 +102,13 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
</ul>
);
}
if (block.kind === 'h5') {
return (
<h5 key={idx} className="mb-1 mt-3 font-semibold text-content first:mt-0" style={fs(12.5, 'body')}>
{block.text}
</h5>
);
}
if (block.kind === 'h4') {
return (
<h4 key={idx} className="mb-1 mt-3 font-geist font-bold uppercase tracking-[.08em] text-content-faint first:mt-0" style={fs(10)}>
@@ -1,4 +1,4 @@
// FE-JRN-SHARE-001 to FE-JRN-SHARE-014
// FE-JRN-SHARE-001 to FE-JRN-SHARE-015
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { http, HttpResponse, delay } from 'msw'
@@ -245,4 +245,15 @@ describe('JourneyShareSection', () => {
expect(plan).toHaveAttribute('aria-pressed', 'false')
expect(screen.getByRole('button', { name: /Recorded routes/ })).toBeDisabled()
})
it('FE-JRN-SHARE-015: a filled accent button sets its words in the colour made for it, not in the accent itself', async () => {
server.use(http.get('/api/journeys/7/share-link', () => HttpResponse.json({ link: existingLink() })))
render(<JourneyShareSection journeyId={JOURNEY_ID} />)
const filled = [await screen.findByRole('button', { name: 'Timeline' }), screen.getByRole('button', { name: 'Copy' })]
for (const button of filled) {
expect(button.className).toContain('text-accent-text')
expect(button.className).not.toContain('text-accent-on')
}
})
})
@@ -69,7 +69,7 @@ export default function JourneyShareSection({ journeyId }: { journeyId: number }
<span className="flex-1 text-caption text-content-muted truncate">{shareUrl}</span>
<button type="button"
onClick={copyLink}
className="flex-shrink-0 px-3 py-1.5 rounded-full bg-accent text-accent-on text-caption font-semibold hover:bg-accent-hover transition-colors"
className="flex-shrink-0 px-3 py-1.5 rounded-full bg-accent text-accent-text text-caption font-semibold hover:bg-accent-hover transition-colors"
>
{copied ? t('journey.share.copied') : t('journey.share.copy')}
</button>
@@ -94,7 +94,7 @@ export default function JourneyShareSection({ journeyId }: { journeyId: number }
onClick={() => togglePerm(key)}
className={`flex items-center gap-2.5 px-3 py-2 rounded-xl border text-body font-medium transition-all ${
link[key]
? 'border-accent bg-accent text-accent-on'
? 'border-accent bg-accent text-accent-text'
: 'border-edge text-content-muted hover:border-content-faint'
}`}
>
@@ -83,6 +83,13 @@ export default function MAdminGitHubPanel({ isPrerelease = false }: { isPrerelea
</ul>
)
}
if (block.kind === 'h5') {
return (
<h5 key={idx} className="text-caption font-semibold mt-3 mb-1 text-m-ink">
{block.text}
</h5>
);
}
if (block.kind === 'h4') {
return (
<h4 key={idx} className="text-xs font-semibold mt-3 mb-1 text-m-ink">
+2 -2
View File
@@ -22,11 +22,11 @@ describe('parseReleaseNotes', () => {
]);
});
it('FE-UTIL-RELNOTES-003: lines are trimmed and markers need their space', () => {
it('FE-UTIL-RELNOTES-003: lines are trimmed, markers need their space and #### heads a feature', () => {
expect(parseReleaseNotes(' ## Spaced \n-no space\n#### deep\n\n\n')).toEqual([
{ kind: 'h3', text: 'Spaced' },
{ kind: 'p', text: '-no space' },
{ kind: 'p', text: '#### deep' },
{ kind: 'h5', text: 'deep' },
]);
});
+7 -4
View File
@@ -1,5 +1,5 @@
/** One block of a release body: a heading, a paragraph, or a run of list items. */
export type ReleaseNoteBlock = { kind: 'h3' | 'h4' | 'p'; text: string } | { kind: 'ul'; items: string[] };
export type ReleaseNoteBlock = { kind: 'h3' | 'h4' | 'h5' | 'p'; text: string } | { kind: 'ul'; items: string[] };
/** Escapes the characters that would let release text break out of the HTML it is placed in. */
export function escapeReleaseHtml(str: string): string {
@@ -7,8 +7,8 @@ export function escapeReleaseHtml(str: string): string {
}
/**
* Splits a GitHub release body into the blocks both admin shells render: `### ` and `## `
* headings, `- ` or `* ` list items gathered into one list until anything else breaks the
* Splits a GitHub release body into the blocks both admin shells render: `## `, `### ` and
* `#### ` headings (the version, a section such as Added, a feature inside it), `- ` or `* ` list items gathered into one list until anything else breaks the
* run, and every other non-empty line as a paragraph. Blank lines only end a list. The
* text stays raw; each shell formats it inline itself.
*/
@@ -30,7 +30,10 @@ export function parseReleaseNotes(body: string): ReleaseNoteBlock[] {
continue;
}
if (trimmed.startsWith('### ')) {
if (trimmed.startsWith('#### ')) {
flushList();
blocks.push({ kind: 'h5', text: trimmed.slice(5) });
} else if (trimmed.startsWith('### ')) {
flushList();
blocks.push({ kind: 'h4', text: trimmed.slice(4) });
} else if (trimmed.startsWith('## ')) {