fix(client): drop the running-sessions badge from the Context Insights entry
The green pill on the sidebar-foot entry read as a notification badge (#74): a running-task count is not something a footer control should surface as an attention marker. The entry returns to a plain icon + label; the count's only consumer was the badge, so the tally helper and the button's useSessions seat plumbing go with it. Fixes #74.
This commit is contained in:
1 parent
b6eda0833c
commit
ffee2a6652
5 files changed
+5
-67
No files matched your search
@@ -3,9 +3,8 @@
|
||||
* above Settings (the harness's own foot layout: footer actions, then the
|
||||
* settings row), mirroring the Settings trigger's geometry in both column
|
||||
* widths. The button carries the plugin emblem and, on the wide
|
||||
* column, its label; a badge counts the sessions currently running (the one
|
||||
* glanceable live fact a footer can carry). Clicking opens the overview
|
||||
* overlay through the shared module store (overviewStore.ts).
|
||||
* column, its label. Clicking opens the overview overlay through the shared
|
||||
* module store (overviewStore.ts).
|
||||
*
|
||||
* The per-user `insightsEntry` preference takes the entry down (renders
|
||||
* null) without unregistering the seat. The subscription fails open: an
|
||||
@@ -15,7 +14,6 @@
|
||||
|
||||
import { useSyncExternalStore, type ReactElement } from 'react'
|
||||
import { ContextIcon } from '../icon'
|
||||
import { runningCountOf, sessionsSnapshotOf } from '../overview'
|
||||
import { overviewStore } from '../overviewStore'
|
||||
import type { ContextSettings, InsightsEntry } from '../settings'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
@@ -23,8 +21,6 @@ import type { ViewKit } from '../viewkit'
|
||||
export interface OverviewButtonProps {
|
||||
/** The footer-action owner share: false on the collapsed 56px rail (icon only). */
|
||||
wide?: boolean
|
||||
/** The root standard kit's sessions seat (absent on a harness without it). */
|
||||
useSessions?: unknown
|
||||
}
|
||||
|
||||
/** Stable subscription faces for the settings-less degrade (no re-subscribes). */
|
||||
@@ -40,8 +36,6 @@ export function makeOverviewButton(kit: ViewKit, settings?: ContextSettings): (p
|
||||
: (listener: () => void): (() => void) => settings.store.subscribe(listener)
|
||||
const getEntry = settings === undefined ? entryShown : (): InsightsEntry => settings.insightsEntry()
|
||||
return function OverviewButton(props: OverviewButtonProps): ReactElement | null {
|
||||
// The hook-level read (unconditional, guarded inside); the badge hides at 0.
|
||||
const running = runningCountOf(sessionsSnapshotOf(props))
|
||||
// The entry toggle: subscribed, so a preference flip takes effect live.
|
||||
const entry = useSyncExternalStore(subscribeEntry, getEntry)
|
||||
if (entry === 'hide') return null
|
||||
@@ -55,7 +49,6 @@ export function makeOverviewButton(kit: ViewKit, settings?: ContextSettings): (p
|
||||
>
|
||||
<ContextIcon size={props.wide === true ? 16 : 18} className="lc-ov-entry-icon" />
|
||||
{props.wide === true && <span className="lc-ov-entry-label">{t('ov.entry')}</span>}
|
||||
{running > 0 && <span className="lc-ov-badge" aria-hidden="true">{running}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,24 +56,6 @@ export function sessionsSnapshotOf(props: { useSessions?: unknown }): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
/** The running-session tally for the footer entry's badge (0 on any shape failure). */
|
||||
export function runningCountOf(snapshot: unknown): number {
|
||||
const state = asRecord(snapshot)
|
||||
if (state === null) return 0
|
||||
const byId = asRecord(state.byId)
|
||||
if (byId === null) return 0
|
||||
let count = 0
|
||||
try {
|
||||
for (const id of Object.keys(byId)) {
|
||||
const row = asRecord(byId[id])
|
||||
if (row !== null && row.running === true) count++
|
||||
}
|
||||
} catch {
|
||||
// A hostile row throwing on access stops the tally at what was counted.
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* The `useWorkspaces` standard prop read — same guarded-hook contract as
|
||||
* {@link sessionsSnapshotOf}. The overview joins its session → workspace
|
||||
|
||||
@@ -15,14 +15,12 @@
|
||||
/* Geometry mirrors the harness's own Settings trigger (ui-settings-general's
|
||||
.trigger): the 42px row, 12px radius, 14px/22px label, and its shared hover
|
||||
fill, so the plugin entry reads as a native foot control, not a visitor. */
|
||||
.lc-ov-entry { position: relative; display: flex; align-items: center; gap: 8px; width: calc(100% + 4px); height: 42px; box-sizing: border-box; margin: 0 -2px; border: 0; padding: 0 10px 0 8px; overflow: hidden; background: transparent; border-radius: 12px; color: var(--dsw-alias-label-primary); font-family: inherit; font-size: 14px; line-height: 22px; cursor: pointer; }
|
||||
.lc-ov-entry { display: flex; align-items: center; gap: 8px; width: calc(100% + 4px); height: 42px; box-sizing: border-box; margin: 0 -2px; border: 0; padding: 0 10px 0 8px; overflow: hidden; background: transparent; border-radius: 12px; color: var(--dsw-alias-label-primary); font-family: inherit; font-size: 14px; line-height: 22px; cursor: pointer; }
|
||||
.lc-ov-entry:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
||||
/* The collapsed rail takes the native foot controls' 36x36 circle, icon only. */
|
||||
.lc-ov-entry-rail { flex: none; justify-content: center; gap: 0; width: 36px; height: 36px; margin: 0; padding: 0; border-radius: 50%; }
|
||||
.lc-ov-entry-icon { flex: none; }
|
||||
.lc-ov-entry-label { flex: 1 1 auto; min-width: 0; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
/* The running-sessions badge: the footer's one live fact, pinned to the icon's corner in both column widths. */
|
||||
.lc-ov-badge { position: absolute; top: 1px; right: 4px; min-width: 15px; height: 15px; box-sizing: border-box; border-radius: 999px; background: var(--color-green-500); color: #fff; font-size: 9px; font-weight: 700; line-height: 1; display: flex; align-items: center; justify-content: center; padding: 0 4px; pointer-events: none; }
|
||||
|
||||
/* ---- the overlay chrome ---- */
|
||||
/* The backdrop carries the `lc-ov` container: it spans the viewport exactly,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// The Context Dashboard's sidebar-foot entry (src/client/components/
|
||||
// overviewButton.tsx): wide/rail rendering, the running badge, the store
|
||||
// flip on click, and the per-user insights-entry toggle.
|
||||
// overviewButton.tsx): wide/rail rendering, the store flip on click, and the
|
||||
// per-user insights-entry toggle.
|
||||
|
||||
import { createElement as h, act } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
@@ -17,9 +17,6 @@ afterEach(() => {
|
||||
overviewStore.set(false)
|
||||
})
|
||||
|
||||
/** A useSessions stand-in over a static snapshot. */
|
||||
const useSessionsOf = (snapshot: unknown) => (<T>(sel: (s: unknown) => T): T => sel(snapshot))
|
||||
|
||||
describe('OverviewButton', () => {
|
||||
test('the wide column renders icon and label; the rail renders the icon alone', async () => {
|
||||
const wide = await mount(h(Button, { wide: true }))
|
||||
@@ -41,23 +38,6 @@ describe('OverviewButton', () => {
|
||||
await bare.unmount()
|
||||
})
|
||||
|
||||
test('the badge counts running sessions and hides at zero', async () => {
|
||||
const running = useSessionsOf({ byId: { a: { running: true }, b: { running: false }, c: { running: true } } })
|
||||
const m = await mount(h(Button, { wide: true, useSessions: running }))
|
||||
assert.equal(query(m.container, '.lc-ov-badge').textContent, '2')
|
||||
await m.unmount()
|
||||
|
||||
const idle = useSessionsOf({ byId: { a: { running: false } } })
|
||||
const calm = await mount(h(Button, { wide: true, useSessions: idle }))
|
||||
assert.equal(queryAll(calm.container, '.lc-ov-badge').length, 0)
|
||||
await calm.unmount()
|
||||
|
||||
// No sessions seat at all: no badge, no throw.
|
||||
const seatless = await mount(h(Button, { wide: true }))
|
||||
assert.equal(queryAll(seatless.container, '.lc-ov-badge').length, 0)
|
||||
await seatless.unmount()
|
||||
})
|
||||
|
||||
test('clicking opens the overview through the shared store', async () => {
|
||||
assert.equal(overviewStore.getSnapshot(), false)
|
||||
const m = await mount(h(Button, { wide: true }))
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
refreshSessions,
|
||||
relativeTime,
|
||||
rowsOfSnapshot,
|
||||
runningCountOf,
|
||||
sessionGroupsOf,
|
||||
sessionsSnapshotOf,
|
||||
sortRows,
|
||||
@@ -89,20 +88,6 @@ describe('sessionsSnapshotOf', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('runningCountOf', () => {
|
||||
test('unusable snapshots tally zero', () => {
|
||||
assert.equal(runningCountOf(null), 0)
|
||||
assert.equal(runningCountOf(7), 0)
|
||||
assert.equal(runningCountOf({ byId: 7 }), 0)
|
||||
})
|
||||
|
||||
test('running rows tally; a hostile row stops the count at what was seen', () => {
|
||||
const hostile = new Proxy({}, { get: () => { throw new Error('boom') } })
|
||||
assert.equal(runningCountOf({ byId: { a: { running: true }, b: { running: false }, c: { running: true } } }), 2)
|
||||
assert.equal(runningCountOf({ byId: { a: { running: true }, x: hostile } }), 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rowsOfSnapshot', () => {
|
||||
test('unusable snapshots read null (the unavailable note, not an empty list)', () => {
|
||||
assert.equal(rowsOfSnapshot(null), null)
|
||||
|
||||
Reference in new issue
Block a user