diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 552be1f348..8ed75b719e 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1945,6 +1945,7 @@ "answer": "Answer", "talk": "Talk", "podsAria": "Pods", + "morePodsAria": "{{count}} more pods", "age": { "now": "now", "minutes": "{{count}}m", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index a390d68913..68b0dbfae4 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1937,6 +1937,7 @@ "answer": "回答", "talk": "对话", "podsAria": "Pod", + "morePodsAria": "还有{{count}}个 Pod", "age": { "now": "刚刚", "minutes": "{{count}}分", diff --git a/frontend/src/v2/__tests__/V2ActivityPage.test.tsx b/frontend/src/v2/__tests__/V2ActivityPage.test.tsx index 1123fd8fa3..879e03bb45 100644 --- a/frontend/src/v2/__tests__/V2ActivityPage.test.tsx +++ b/frontend/src/v2/__tests__/V2ActivityPage.test.tsx @@ -250,6 +250,50 @@ describe('V2ActivityPage', () => { expect(second).toHaveAttribute('aria-describedby', expect.stringContaining('description')); }); + test('clamps long queue details until the reader expands them', async () => { + const detail = 'This queue detail is intentionally long so the compact Activity row can reveal the complete context on demand.'; + const queueWithLongDetail = { + ...decisionQueue, + items: [{ ...decisionQueue.items[0], detail }], + count: 1, + }; + mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? queueWithLongDetail : recap })); + renderPage(); + + const detailButton = await screen.findByRole('button', { name: detail }); + expect(detailButton).toHaveClass('v2-activity__queue-detail'); + expect(detailButton).toHaveAttribute('aria-expanded', 'false'); + fireEvent.click(detailButton); + expect(detailButton).toHaveAttribute('aria-expanded', 'true'); + fireEvent.click(detailButton); + expect(detailButton).toHaveAttribute('aria-expanded', 'false'); + }); + + test('a keyboard user with focus on the detail collapses it on the second toggle', async () => { + const detail = 'Keyboard focus must not hold a queue detail open after the second Enter.'; + const queueWithLongDetail = { + ...decisionQueue, + items: [{ ...decisionQueue.items[0], detail }], + count: 1, + }; + mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? queueWithLongDetail : recap })); + renderPage(); + + const detailButton = await screen.findByRole('button', { name: detail }); + // Focus stays on the button across both toggles, as it does for Enter on a focused control. + detailButton.focus(); + fireEvent.click(detailButton); + expect(detailButton).toHaveClass('v2-activity__queue-detail--expanded'); + fireEvent.click(detailButton); + expect(detailButton).not.toHaveClass('v2-activity__queue-detail--expanded'); + expect(detailButton).toHaveClass('v2-activity__queue-detail--dismissed'); + expect(detailButton).toHaveAttribute('aria-expanded', 'false'); + + // Blur clears the dismissal, so a later Tab-in previews the full copy again. + fireEvent.blur(detailButton); + expect(detailButton).not.toHaveClass('v2-activity__queue-detail--dismissed'); + }); + test('changes the read window and opens the source pod from a factual queue row', async () => { renderPage(); await screen.findByText('Review requested'); @@ -317,7 +361,7 @@ describe('V2ActivityPage', () => { expect(screen.getByText('Nothing needs you.')).toBeInTheDocument(); expect(screen.queryByLabelText(/waiting on you/)).not.toBeInTheDocument(); // Nobody to wake: the composer is hidden while step 1 is open. - expect(screen.queryByRole('heading', { name: /tell your agents/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).not.toBeInTheDocument(); const card = screen.getByRole('heading', { name: 'Get started' }).closest('section') as HTMLElement; expect(card.compareDocumentPosition(screen.getByRole('heading', { name: 'Needs you' })) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: 'Hire an agent' })); @@ -333,7 +377,7 @@ describe('V2ActivityPage', () => { expect(await screen.findByText('2 steps · until your first ask arrives')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Hire an agent' })).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Open Launch pod' })).toHaveClass('v2-activity__start-cta--current'); - expect(screen.getByRole('heading', { name: /tell your agents/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).toBeInTheDocument(); second.unmount(); // The account spoke, reply or not: step 2 leaves — with the connector row too, no card at all. @@ -436,9 +480,9 @@ describe('V2ActivityPage', () => { renderPage(); - expect(await screen.findByRole('heading', { name: /tell your agents/i })).toBeInTheDocument(); + expect(await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).toBeInTheDocument(); await act(async () => { releaseRegistry?.({ data: { agents: [] } }); }); - await waitFor(() => expect(screen.queryByRole('heading', { name: /tell your agents/i })).not.toBeInTheDocument()); + await waitFor(() => expect(screen.queryByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).not.toBeInTheDocument()); expect(screen.queryByRole('heading', { name: 'Get started' })).not.toBeInTheDocument(); expect(screen.getByText('Nothing needs you.')).toBeInTheDocument(); }); @@ -456,9 +500,9 @@ describe('V2ActivityPage', () => { renderPage(); - expect(await screen.findByRole('heading', { name: /tell your agents/i })).toBeInTheDocument(); + expect(await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).toBeInTheDocument(); await act(async () => { releaseRegistry?.({ data: { agents: [] } }); }); - await waitFor(() => expect(screen.queryByRole('heading', { name: /tell your agents/i })).not.toBeInTheDocument()); + await waitFor(() => expect(screen.queryByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).not.toBeInTheDocument()); expect(screen.queryByRole('heading', { name: 'Get started' })).not.toBeInTheDocument(); }); @@ -1346,7 +1390,7 @@ describe('V2ActivityPage', () => { const activityRecap = { ...recap, pods: [...recap.pods, { id: 'pod-2', name: 'GTM Programs' }] }; mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? decisionQueue : activityRecap })); renderPage(); - await screen.findByRole('heading', { name: 'Tell your agents' }); + await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') }); const picker = document.querySelector('.v2-activity__compose-picker-button'); expect(picker).not.toBeNull(); fireEvent.click(picker); @@ -1377,7 +1421,7 @@ describe('V2ActivityPage', () => { const activityRecap = { ...recap, pods: [...recap.pods, { id: 'pod-2', name: 'GTM Programs' }] }; mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? decisionQueue : activityRecap })); renderPage(); - await screen.findByRole('heading', { name: 'Tell your agents' }); + await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') }); const picker = document.querySelector('.v2-activity__compose-picker-button'); expect(picker).not.toBeNull(); fireEvent.click(picker); @@ -1392,7 +1436,7 @@ describe('V2ActivityPage', () => { const activityRecap = { ...recap, pods: [...recap.pods, { id: 'pod-2', name: 'GTM Programs' }] }; mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? decisionQueue : activityRecap })); renderPage(); - await screen.findByRole('heading', { name: 'Tell your agents' }); + await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') }); const picker = document.querySelector('.v2-activity__compose-picker-button'); expect(picker).not.toBeNull(); picker.focus(); @@ -1406,7 +1450,7 @@ describe('V2ActivityPage', () => { const activityRecap = { ...recap, pods: [...recap.pods, { id: 'pod-2', name: 'GTM Programs' }] }; mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? decisionQueue : activityRecap })); renderPage(); - await screen.findByRole('heading', { name: 'Tell your agents' }); + await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') }); const picker = document.querySelector('.v2-activity__compose-picker-button'); expect(picker).not.toBeNull(); fireEvent.click(picker); diff --git a/frontend/src/v2/__tests__/V2YourTeamSignal.test.tsx b/frontend/src/v2/__tests__/V2YourTeamSignal.test.tsx index b1c60a30e9..f8a31fa8fd 100644 --- a/frontend/src/v2/__tests__/V2YourTeamSignal.test.tsx +++ b/frontend/src/v2/__tests__/V2YourTeamSignal.test.tsx @@ -34,10 +34,13 @@ const agents = { const renderPage = (over = {}) => { const queue = over.queue ?? [{ id: 'd-1', kind: 'decision', title: 'Slack defaults', podId: 'p2', messageId: '900', actorUserId: 'wren-user' }]; + const podList = over.pods ?? [{ _id: 'p1', name: 'Sharpen — pod model, attention routing, hardening' }, { _id: 'p2', name: 'Connectors v2 — channel routing' }]; + const agentRows = over.agents ?? agents; axios.get.mockImplementation((url) => { - if (url === '/api/pods') return Promise.resolve({ data: [{ _id: 'p1', name: 'Sharpen — pod model, attention routing, hardening' }, { _id: 'p2', name: 'Connectors v2 — channel routing' }] }); - if (url.startsWith('/api/registry/pods/p1/agents')) return Promise.resolve({ data: { agents: over.p1Agents || agents.p1 } }); - if (url.startsWith('/api/registry/pods/p2/agents')) return Promise.resolve({ data: { agents: agents.p2 } }); + if (url === '/api/pods') return Promise.resolve({ data: podList }); + if (url.startsWith('/api/registry/pods/p1/agents')) return Promise.resolve({ data: { agents: over.p1Agents || agentRows.p1 } }); + if (url.startsWith('/api/registry/pods/p2/agents')) return Promise.resolve({ data: { agents: agentRows.p2 } }); + if (url.startsWith('/api/registry/pods/p3/agents')) return Promise.resolve({ data: { agents: agentRows.p3 || [] } }); // `claimedBy` is the claimer's User id (never a name); `assignee` is a name // written by PATCH and is not a claim. if (url.startsWith('/api/v1/tasks/p1')) return Promise.resolve({ data: { tasks: [{ taskId: 'TASK-131', status: 'claimed', claimedBy: 'kai-user' }, { taskId: 'TASK-140', status: 'claimed', claimedBy: null, assignee: 'sage' }] } }); @@ -140,6 +143,31 @@ describe('Your Team (direction C)', () => { expect(card.querySelector('.v2-team-card__desc')).toHaveTextContent('Fixes. Small PRs with a test each.'); }); + test('hides template descriptions and folds extra pods into a +N chip', async () => { + renderPage({ + pods: [ + { _id: 'p1', name: 'Sharpen — pod model, attention routing, hardening' }, + { _id: 'p2', name: 'Connectors v2 — channel routing' }, + { _id: 'p3', name: 'Growth — signup traces' }, + ], + agents: { + ...agents, + p1: [...agents.p1, { + name: 'template-agent', instanceId: 'default', displayName: 'Template', userId: 'template-user', + lastActiveAt: minutesAgo(4), description: 'Your friendly AI assistant powered by Claude…', + }], + p3: [{ name: 'wren', instanceId: 'default', displayName: 'Wren', userId: 'wren-user', lastActiveAt: minutesAgo(30) }], + }, + }); + await waitFor(() => expect(screen.getByText('Template')).toBeInTheDocument()); + + expect(cardOf('Template').querySelector('.v2-team-card__desc')).toBeNull(); + const wren = cardOf('Wren'); + expect(wren.querySelectorAll('.v2-team-card__pod')).toHaveLength(3); + expect(wren.querySelector('.v2-team-card__pod--more')).toHaveTextContent('+1'); + expect(wren.querySelector('.v2-team-card__pod--more')).toHaveAttribute('aria-label', '1 more pods'); + }); + test('idle: divider mark, muted `idle · 41m`, and NO description line when the listing has none — never a quote', async () => { renderPage(); await waitFor(() => expect(screen.getByText('Sage')).toBeInTheDocument()); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 4b8ff883a9..c81e9872f3 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -168,6 +168,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { const mobileTabs = read('../components/V2MobileTabs.tsx'); const podBoard = read('../components/V2PodBoard.tsx'); const activityPage = read('../components/V2ActivityPage.tsx'); + const yourTeamPage = read('../components/V2YourTeamPage.tsx'); const featurePage = read('../components/V2FeaturePage.tsx'); const v2App = read('../V2App.tsx'); const app = read('../../App.tsx'); @@ -259,6 +260,12 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(rule).not.toContain('white-space: nowrap'); }); + test('Your Team keeps template copy out of cards and folds long pod lists', () => { + expect(yourTeamPage).toContain('isTemplateDescription(description, display)'); + expect(yourTeamPage).toContain("(a.podNames || []).slice(0, 2)"); + expect(yourTeamPage).toContain('v2-team-card__pod--more'); + }); + test('the workspace grid owns the measured 56 / 260 / fluid / 272 geometry', () => { // Direction C (walk-1 ruling a, 2026-09-06): 14px page padding and three // 14px gaps leave the 782px chat track the artboard measured. @@ -564,6 +571,32 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(ruleBody(v2, '.v2-root .v2-activity__queue-actions button')).toContain('min-height: 32px'); }); + test('Activity details clamp to two lines and reveal full copy on hover, focus, or expand', () => { + const detail = ruleBody(v2, '.v2-root .v2-activity__queue-detail'); + expect(detail).toContain('display: -webkit-box'); + expect(detail).toContain('-webkit-line-clamp: 2'); + expect(detail).toContain('overflow: hidden'); + const reveal = selectorRuleBody(v2, '.v2-root .v2-activity__queue-detail--expanded'); + expect(reveal).toContain('display: block'); + // Tab-in preview is kept; a reader's collapse is excluded so focus can't re-open it (Lumen HOLD on #2407). + const focusReveal = selectorRuleBody(v2, '.v2-root .v2-activity__queue-detail:focus-visible:not(.v2-activity__queue-detail--dismissed)'); + expect(focusReveal).toContain('display: block'); + expect(focusReveal).toContain('-webkit-line-clamp: unset'); + expect(activityPage).toContain('v2-activity__queue-detail--dismissed'); + expect(reveal).toContain('-webkit-line-clamp: unset'); + const baseDetail = selectorRuleBody(v2, '.v2-root button.v2-activity__queue-detail'); + expect(baseDetail).toContain('color: var(--v2-text-secondary)'); + const mentionDetail = selectorRuleBody(v2, '.v2-root .v2-activity__queue-row--mention .v2-activity__queue-detail'); + expect(mentionDetail).toContain('color: var(--v2-text-primary)'); + expect(v2).toContain('.v2-root:lang(zh) .v2-activity__queue-detail,'); + expect(v2).toContain('.v2-root .v2-activity__queue-detail:hover:not(:focus)'); + expect(v2).toContain('@media (hover: hover)'); + expect(activityPage).toContain('aria-expanded={expandedDetailIds.has(item.id)}'); + expect(activityPage).toContain('v2-activity__queue-detail'); + expect(activityPage).toContain('aria-label={t(\'activity.compose.placeholder\')}'); + expect(activityPage).not.toContain('v2-activity__compose-label'); + }); + test('Activity pagination keeps the Show more affordance visible and keyboard-sized', () => { const more = ruleBody(v2, '.v2-activity__queue-more'); expect(more).toContain('min-height: 36px'); @@ -1196,6 +1229,22 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(chip).toContain('font-size: 12px'); }); + test('UI-STRUCT B3 uses ink for primary actions and mono counts without pills', () => { + const boardNew = ruleBody(v2, '.v2-root button.v2-board__new'); + expect(boardNew).toContain('background: var(--v2-ink)'); + expect(boardNew).toContain('color: var(--v2-on-ink)'); + + const columnCount = ruleBody(v2, '.v2-board__col-count'); + expect(columnCount).toContain('var(--v2-font-mono)'); + expect(columnCount).not.toContain('background:'); + expect(columnCount).not.toContain('border-radius:'); + expect(columnCount).not.toContain('padding:'); + + const connectCta = lastRuleBody(v2, '.v2-root .v2-connect__cta'); + expect(connectCta).toContain('background: var(--v2-ink)'); + expect(connectCta).toContain('color: var(--v2-on-ink)'); + }); + test('the board craft-pass buttons use icon components instead of unicode glyphs', () => { // This is structural: accessible labels cannot prove a literal glyph has // left the JSX. The MUI imports and icon nodes are the implementation diff --git a/frontend/src/v2/components/V2ActivityPage.tsx b/frontend/src/v2/components/V2ActivityPage.tsx index 7ddbbb77e7..47c46fce4b 100644 --- a/frontend/src/v2/components/V2ActivityPage.tsx +++ b/frontend/src/v2/components/V2ActivityPage.tsx @@ -229,6 +229,10 @@ const V2ActivityPage: React.FC = () => { const historyOffsetRef = useRef(0); const pendingRefreshFocusRef = useRef(null); const [replyOpenIds, setReplyOpenIds] = useState>(new Set()); + const [expandedDetailIds, setExpandedDetailIds] = useState>(new Set()); + // A reader who collapsed a detail keeps focus on it; this stops the focus + // preview from re-opening it until the button is blurred and focused again. + const [dismissedDetailIds, setDismissedDetailIds] = useState>(new Set()); const [replyDrafts, setReplyDrafts] = useState>({}); const [composePodId, setComposePodId] = useState(''); const [composeDraft, setComposeDraft] = useState(''); @@ -1124,9 +1128,8 @@ const V2ActivityPage: React.FC = () => { <> {/* The composer stays hidden while step 1 is open — nobody to wake — and returns the moment an agent exists. */} {!startSteps.includes('hire') && ( -
+
-

{t('activity.compose.label')}

{t('activity.compose.podLabel')}
@@ -1272,7 +1275,36 @@ const V2ActivityPage: React.FC = () => {
{item.kind === 'mention' && item.actorName ? item.actorName : item.title}
- {item.detail &&

{item.detail}

} + {item.detail && ( + + )}
{item.kind === 'approval' && ( diff --git a/frontend/src/v2/components/V2YourTeamPage.tsx b/frontend/src/v2/components/V2YourTeamPage.tsx index 43692f6837..14bee1a57c 100644 --- a/frontend/src/v2/components/V2YourTeamPage.tsx +++ b/frontend/src/v2/components/V2YourTeamPage.tsx @@ -107,6 +107,14 @@ const agentKey = (a: { name: string; instanceId?: string }) => `${a.name}:${a.in // chip and the status line carry the name before the subtitle. const shortPodName = (name: string): string => name.split(' — ')[0].split(' - ')[0].trim(); +const isTemplateDescription = (description: string, displayName: string): boolean => { + const value = description.trim().toLowerCase(); + const nameTemplate = `${displayName.trim().toLowerCase()} agent`; + return value === ' agent' + || value === nameTemplate + || value.startsWith('your friendly ai assistant powered by claude'); +}; + // A seat can be installed in several pods; the listing is per pod. Fold to // one card per identity and keep every pod's name for the chips. const dedupeAgents = (rows: Array): AgentInstallationSummary[] => { @@ -326,6 +334,7 @@ const V2YourTeamPage: React.FC = () => { const renderCard = ({ a, state }: { a: AgentInstallationSummary; state: CardState }) => { const display = a.displayName || a.name; + const description = a.description?.trim(); const key = agentKey(a); const ask = a.userId ? asksByActor.get(a.userId)?.[0] : undefined; const task = claimedTaskFor(a); @@ -363,12 +372,17 @@ const V2YourTeamPage: React.FC = () => {
- {a.description &&

{a.description}

} + {description && !isTemplateDescription(description, display) &&

{description}

} {(a.podNames?.length || 0) > 0 && (
- {(a.podNames || []).slice(0, 4).map((name) => ( + {(a.podNames || []).slice(0, 2).map((name) => ( {shortPodName(name).toLowerCase()} ))} + {(a.podNames || []).length > 2 && ( + + +{(a.podNames || []).length - 2} + + )}
)}
diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 0864a7228c..c402275f9b 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -6737,14 +6737,14 @@ body.modern-ui.v2-canvas { } .v2-root .v2-connect__cta { - border: 1px solid var(--v2-accent); - background: var(--v2-accent); - color: #fff; + border: 1px solid var(--v2-ink); + background: var(--v2-ink); + color: var(--v2-on-ink); } .v2-root .v2-connect__cta:hover:not(:disabled) { - border-color: var(--v2-accent-strong); - background: var(--v2-accent-strong); + border-color: var(--v2-ink-hover); + background: var(--v2-ink-hover); } .v2-root .v2-connect__cta:disabled { @@ -8326,8 +8326,8 @@ body.modern-ui.v2-canvas { align-items: center; gap: 6px; border: none; - background: var(--v2-accent); - color: #fff; + background: var(--v2-ink); + color: var(--v2-on-ink); font-size: 13px; font-weight: 600; padding: 8px 14px; @@ -8337,7 +8337,7 @@ body.modern-ui.v2-canvas { } .v2-root button.v2-board__new:hover { - background: var(--v2-accent-strong); + background: var(--v2-ink-hover); } .v2-board__focus { @@ -8706,12 +8706,8 @@ body.modern-ui.v2-canvas { } .v2-board__col-count { - font-size: 12px; - font-weight: 600; - color: var(--v2-text-tertiary); - background: var(--v2-surface-tint); - border-radius: 999px; - padding: 1px 8px; + color: var(--v2-text-secondary); + font: 500 11px/16px var(--v2-font-mono); } .v2-board__col-empty { @@ -9708,17 +9704,6 @@ body.modern-ui.v2-canvas { gap: 12px; } -.v2-activity__compose h2.v2-activity__compose-label { - margin: 0; - color: var(--v2-text-muted); - font-size: var(--v2-fs-label); - font-family: var(--v2-font-mono); - line-height: var(--v2-lh-label); - font-weight: 600; - letter-spacing: 0.06em; - text-transform: uppercase; -} - .v2-activity__compose-pod { display: inline-flex; align-items: center; @@ -10110,6 +10095,57 @@ body.modern-ui.v2-canvas { line-height: var(--v2-lh-body); } +/* Activity details stay compact until the reader hovers, focuses, or taps to + expand them. The button keeps the full copy available without adding a + permanent paragraph to every queue row. */ +.v2-root .v2-activity__queue-detail { + display: -webkit-box; + min-height: 0; + max-width: 100%; + margin: 3px 0; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + color: var(--v2-text-secondary); + font: 400 var(--v2-fs-body)/var(--v2-lh-body) var(--v2-font); + text-align: left; + white-space: normal; + overflow: hidden; + overflow-wrap: anywhere; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.v2-root button.v2-activity__queue-detail { + color: var(--v2-text-secondary); +} + +.v2-root .v2-activity__queue-row--mention .v2-activity__queue-detail { + color: var(--v2-text-primary); +} + +/* Focus previews the full copy (Tab in), but a detail the reader just + collapsed is marked --dismissed and stays closed while focus remains on it, + so aria-expanded and the visible copy agree. Blur clears the mark. */ +.v2-root .v2-activity__queue-detail:focus-visible:not(.v2-activity__queue-detail--dismissed), +.v2-root .v2-activity__queue-detail--expanded { + display: block; + overflow: visible; + -webkit-line-clamp: unset; +} + +/* Pointer hover is only a reveal affordance on devices that have hover. A + focused button is intentionally excluded: after a pointer click the focus + must not keep a second click from collapsing the detail again. */ +@media (hover: hover) { + .v2-root .v2-activity__queue-detail:hover:not(:focus) { + display: block; + overflow: visible; + -webkit-line-clamp: unset; + } +} + .v2-activity__queue-actions, .v2-activity__queue-row button, .v2-activity__board-meta button { @@ -10547,7 +10583,6 @@ body.modern-ui.v2-canvas { .v2-root:lang(zh) .v2-feature__legacy .MuiTypography-h5, .v2-root:lang(zh) .v2-feature__legacy .MuiTypography-h6, .v2-root:lang(zh) .v2-activity__title, -.v2-root:lang(zh) .v2-activity__compose-label, .v2-root:lang(zh) .v2-activity__section h2, .v2-root:lang(zh) .v2-activity__agent-card h3, .v2-root:lang(zh) .v2-activity__board-row h3, @@ -10596,6 +10631,7 @@ body.modern-ui.v2-canvas { .v2-root:lang(zh) .v2-pods__discover-description, .v2-root:lang(zh) .v2-pods__community-copy, .v2-root:lang(zh) .v2-activity__queue-copy p, +.v2-root:lang(zh) .v2-activity__queue-detail, .v2-root:lang(zh) .v2-activity__board-row p, .v2-root:lang(zh) .v2-activity__agent-recap, .v2-root:lang(zh) .v2-activity__empty, @@ -12882,8 +12918,6 @@ body.modern-ui.v2-canvas { /* Opening a reply or a free-text ruling gives that editor a full content row. */ .v2-activity__queue-row .v2-activity__queue-actions:has(textarea) { grid-column: 2 / -1; grid-row: 2; max-width: none; justify-content: flex-start; } .v2-activity__empty--plain span { color: var(--v2-text-secondary); font: 400 13px/18px var(--v2-font); } -.v2-activity__compose h2.v2-activity__compose-label { font: 600 11px/16px var(--v2-font-mono); } - /* Keep filters on a separate row when the pane cannot fit them beside the title. Both rows stay in normal reading order, without a clipped title. */ @media (max-width: 1100px) {