diff --git a/test/ui-actions.test.mjs b/test/ui-actions.test.mjs new file mode 100644 index 0000000..75778cf --- /dev/null +++ b/test/ui-actions.test.mjs @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const cardSource = readFileSync(new URL('../ui/ContributionCard.jsx', import.meta.url), 'utf8') +const stackSource = readFileSync(new URL('../ui/ContributionStack.jsx', import.meta.url), 'utf8') + +test('send actions keep a visible label instead of relying on the icon alone', () => { + assert.match(cardSource, /sending \? 'Sending…' : 'Send'/) + assert.match(stackSource, /Send<\/span>/) +}) + +test('single and stacked sends expose elapsed progress to assistive technology', () => { + for (const source of [cardSource, stackSource]) { + assert.match(source, /role="status" aria-live="polite"/) + assert.match(source, /sendElapsed/) + } +}) diff --git a/theme.js b/theme.js index b620e1c..1c9cee0 100644 --- a/theme.js +++ b/theme.js @@ -947,6 +947,10 @@ export const CSS = ` font: inherit; cursor: pointer; transition: color .14s ease, border-color .14s ease, background .14s ease, transform .1s ease; } +.co-send-btn { + width: auto; min-width: 76px; padding: 0 12px; gap: 7px; + font-size: 12.5px; font-weight: 700; +} .co-icon-btn.is-primary { border-color: color-mix(in srgb, var(--accent) 38%, var(--border)); background: color-mix(in srgb, var(--accent) 11%, var(--surface)); diff --git a/ui/ContributionCard.jsx b/ui/ContributionCard.jsx index 0a1baea..92f88b8 100644 --- a/ui/ContributionCard.jsx +++ b/ui/ContributionCard.jsx @@ -235,6 +235,7 @@ function ReviewPlan({ rec, loadDiff }) { function ReviewActions({ rec, reviewState, onSend, onFeedback, onDismiss }) { const [sendNote, setSendNote] = useState(null) const [sending, setSending] = useState(false) + const [sendElapsed, setSendElapsed] = useState(0) const [dismissing, setDismissing] = useState(false) // Dismiss abandons the prepared record (a CAS flip the skill treats as // terminal), so it must never fire on a single stray tap. The first tap arms @@ -252,6 +253,18 @@ function ReviewActions({ rec, reviewState, onSend, onFeedback, onDismiss }) { if (confirmingDismiss) keepButtonRef.current?.focus() }, [confirmingDismiss]) + useEffect(() => { + if (!sending) { + setSendElapsed(0) + return undefined + } + const startedAt = Date.now() + const update = () => setSendElapsed(Math.floor((Date.now() - startedAt) / 1000)) + update() + const timer = window.setInterval(update, 1000) + return () => window.clearInterval(timer) + }, [sending]) + async function send() { if (!isPr || blocked) return setSending(true) @@ -343,7 +356,7 @@ function ReviewActions({ rec, reviewState, onSend, onFeedback, onDismiss }) { {isPr ? ( ) : null} + {sending ? ( +

+ Publishing the reviewed pull requests in order + {sendElapsed >= 5 ? ` · ${sendElapsed}s elapsed` : '…'} +

+ ) : null} ) : (