Skip to content

Commit 7acec83

Browse files
authored
feat(ui): implements compact view density mode (#55)
* feat(ui): implements compact view density mode * refactor(ui): migrates compact density to Tailwind custom variant * fix(ui): adds compact layout tests and restores hasCompactTooltip guard * feat(ui): integrates tracked tab with compact density * docs: updates tracked tab and auto-removal in user guide * docs: updates dashboard screenshot * fix(test): clears lingering Kobalte timers after each test * fix(ui): adds compact:tab-sm to tracked tab, memoizes derived values * perf(ui): memoizes derived roles and size category in TrackedTab
1 parent 534a7b4 commit 7acec83

29 files changed

Lines changed: 648 additions & 278 deletions

docs/USER_GUIDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,11 +335,11 @@ The Tracked tab lets you pin issues and PRs into a personal TODO list that you c
335335

336336
**Pinning items:** On the Issues and Pull Requests tabs, hover over any row to reveal a bookmark icon. Click it to pin the item to your tracked list. Click it again to unpin. The bookmark appears filled and highlighted on tracked items.
337337

338-
**Tracked tab:** Shows your pinned items in a flat list (not grouped by repo). Each item displays a type badge (Issue or PR) and uses live data from the poll cycle — statuses, check results, and labels stay current. Items whose repo is no longer being polled show a minimal fallback row with stored metadata.
338+
**Tracked tab:** Shows your pinned items in a flat list (not grouped by repo). Each item shows a repo badge, a type badge (Issue or PR), and uses live data from the poll cycle — labels, comments, and timestamps stay current. Tracked PRs display the same metadata as the Pull Requests tab: review status, size badge, check status dot, draft indicator, and role badge. Tracked issues show a role badge (author/assignee). In compact density, the repo badge abbreviates to just the repo name (hover for the full owner/repo). Items whose repo is no longer being polled show a minimal fallback row with stored metadata. PRs being hot-polled show a shimmer animation and a spinner in the left margin.
339339

340340
**Reordering:** Use the chevron buttons on the left side of each row to move items up or down. Items slide smoothly into their new position.
341341

342-
**Auto-removal:** When a tracked issue is closed or a tracked PR is merged, it is automatically removed from the list. Closure is detected by absence from the `is:open` poll results. For PRs detected as closed by the hot poll, removal happens within seconds.
342+
**Auto-removal:** When a tracked issue is closed or a tracked PR is merged, it is automatically removed from the list. Closure is detected by absence from the `is:open` poll results. For PRs detected as closed by the hot poll, removal happens within seconds. Auto-removal is suspended when the API returns errors (e.g., rate limiting) to prevent false pruning.
343343

344344
**Relationship to other features:** The Tracked tab bypasses the org/repo filter — it always shows all your pinned items regardless of which repo filter is active. Ignoring an item from the Issues or Pull Requests tab also removes it from the tracked list. The tracked list is preserved when tracking is disabled and restored when re-enabled.
345345

docs/dashboard-screenshot.png

-13.8 KB
Loading

src/app/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ export default function App() {
147147
document.documentElement.setAttribute("data-theme", resolveTheme(config.theme));
148148
});
149149

150+
createEffect(() => {
151+
document.documentElement.dataset.density = config.viewDensity;
152+
});
153+
150154
onMount(() => {
151155
// Listen for system theme changes so "auto" reacts immediately
152156
const mq = window.matchMedia("(prefers-color-scheme: dark)");

src/app/components/dashboard/ActionsTab.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { createEffect, createMemo, For, Show } from "solid-js";
22
import { createStore } from "solid-js/store";
33
import type { WorkflowRun } from "../../services/api";
4-
import { config } from "../../stores/config";
54
import { viewState, setViewState, setTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, type ActionsFilterField } from "../../stores/view";
65
import WorkflowSummaryCard from "./WorkflowSummaryCard";
76
import IgnoreBadge from "./IgnoreBadge";
@@ -221,8 +220,8 @@ export default function ActionsTab(props: ActionsTabProps) {
221220
return (
222221
<div class="divide-y divide-base-300">
223222
{/* Toolbar */}
224-
<div class="flex items-start gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
225-
<div class="flex flex-wrap items-center gap-3 min-w-0 flex-1">
223+
<div class="flex items-start px-4 py-2 gap-3 compact:py-0.5 compact:gap-2 border-b border-base-300 bg-base-100">
224+
<div class="flex flex-wrap items-center min-w-0 flex-1 gap-3 compact:gap-2">
226225
<label class="flex items-center gap-1.5 text-sm text-base-content/70 cursor-pointer select-none">
227226
<input
228227
type="checkbox"
@@ -299,7 +298,7 @@ export default function ActionsTab(props: ActionsTabProps) {
299298
<button
300299
onClick={() => toggleExpandedRepo("actions", repoGroup.repoFullName)}
301300
aria-expanded={isExpanded()}
302-
class="flex-1 flex items-center gap-2 px-4 py-2.5 text-left text-sm font-semibold text-base-content"
301+
class="flex-1 flex items-center gap-2 px-4 py-2.5 compact:py-1.5 text-left text-sm font-semibold text-base-content"
303302
>
304303
<ChevronIcon size="md" rotated={!isExpanded()} />
305304
{repoGroup.repoFullName}
@@ -356,7 +355,6 @@ export default function ActionsTab(props: ActionsTabProps) {
356355
expanded={isWfExpanded()}
357356
onToggle={() => toggleWorkflow(wfKey)}
358357
onIgnoreRun={handleIgnore}
359-
density={config.viewDensity}
360358
refreshTick={props.refreshTick}
361359
hotPollingRunIds={props.hotPollingRunIds}
362360
flashingRunIds={flashingRunIds()}

src/app/components/dashboard/DashboardPage.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ function resetDashboardData(): void {
9898
const [hasFetchedFresh, setHasFetchedFresh] = createSignal(false);
9999
export function _resetHasFetchedFresh(value = false) { setHasFetchedFresh(value); }
100100

101+
const [lastFetchHadErrors, setLastFetchHadErrors] = createSignal(false);
102+
101103
// Clear dashboard data and stop polling on logout to prevent cross-user data leakage
102104
onAuthCleared(() => {
103105
resetDashboardData();
@@ -144,6 +146,17 @@ async function pollFetch(): Promise<DashboardData> {
144146
});
145147
// When notifications gate says nothing changed, keep existing data
146148
if (!data.skipped) {
149+
const hasErrors = data.errors.length > 0;
150+
setLastFetchHadErrors(hasErrors);
151+
152+
// When the fetch had errors and returned no data, keep stale dashboard
153+
// visible rather than wiping it to empty. This prevents the summary strip,
154+
// tab counts, and tracked items from vanishing during rate limiting.
155+
if (hasErrors && data.issues.length === 0 && data.pullRequests.length === 0 && data.workflowRuns.length === 0) {
156+
setDashboardData("loading", false);
157+
return data;
158+
}
159+
147160
setHasFetchedFresh(true);
148161
const now = new Date();
149162

@@ -254,6 +267,22 @@ export default function DashboardPage() {
254267
const [hotPollingPRIds, setHotPollingPRIds] = createSignal<ReadonlySet<number>>(new Set());
255268
const [hotPollingRunIds, setHotPollingRunIds] = createSignal<ReadonlySet<number>>(new Set());
256269

270+
if (import.meta.env.DEV) {
271+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
272+
(window as any).__debug = {
273+
forceHotPoll: () => {
274+
const allPrIds = new Set<number>(dashboardData.pullRequests.map(pr => pr.id));
275+
setHotPollingPRIds(allPrIds);
276+
console.info(`[debug] Shimmer ON for ${allPrIds.size} PRs. Call __debug.clearHotPoll() to stop.`);
277+
},
278+
clearHotPoll: () => {
279+
setHotPollingPRIds(new Set<number>());
280+
setHotPollingRunIds(new Set<number>());
281+
console.info("[debug] Shimmer OFF");
282+
},
283+
};
284+
}
285+
257286
function resolveInitialTab(): TabId {
258287
const tab = config.rememberLastTab ? viewState.lastActiveTab : config.defaultTab;
259288
if (tab === "tracked" && !config.enableTracking) return "issues";
@@ -283,6 +312,9 @@ export default function DashboardPage() {
283312
const issues = dashboardData.issues;
284313
const prs = dashboardData.pullRequests;
285314
if (!config.enableTracking || viewState.trackedItems.length === 0 || !hasFetchedFresh()) return;
315+
// Never prune when the last fetch had errors (rate limit, network failure, etc.)
316+
// — the missing items are likely just unfetched, not closed/merged.
317+
if (lastFetchHadErrors()) return;
286318

287319
const polledRepos = new Set([
288320
...config.selectedRepos.map((r) => r.fullName),
@@ -506,6 +538,8 @@ export default function DashboardPage() {
506538
issues={dashboardData.issues}
507539
pullRequests={dashboardData.pullRequests}
508540
refreshTick={refreshTick()}
541+
userLogin={userLogin()}
542+
hotPollingPRIds={hotPollingPRIds()}
509543
/>
510544
</Match>
511545
<Match when={activeTab() === "actions"}>

src/app/components/dashboard/IgnoreBadge.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,16 @@ export default function IgnoreBadge(props: IgnoreBadgeProps) {
4747
<Tooltip content={`${props.items.length} ignored item${props.items.length === 1 ? "" : "s"}`}>
4848
<button
4949
onClick={() => setOpen((v) => !v)}
50-
class="btn btn-ghost btn-sm relative"
50+
class="btn btn-ghost btn-sm compact:btn-xs relative"
5151
aria-haspopup="true"
5252
aria-expanded={open()}
5353
aria-label={`${props.items.length} ignored items`}
5454
>
55-
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
55+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 compact:h-3.5 compact:w-3.5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
5656
<path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd" />
5757
<path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z" />
5858
</svg>
59-
<span class="badge badge-neutral badge-xs absolute -top-1 -right-1">{props.items.length}</span>
59+
<span class="badge badge-neutral badge-xs absolute -top-1 -right-1 compact:text-[8px] compact:-top-0.5 compact:-right-0.5 compact:px-0.5 compact:min-w-3 compact:h-3">{props.items.length}</span>
6060
</button>
6161
</Tooltip>
6262

src/app/components/dashboard/IssuesTab.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,8 @@ export default function IssuesTab(props: IssuesTabProps) {
255255
return (
256256
<div class="flex flex-col h-full">
257257
{/* Filter chips + ignore badge toolbar */}
258-
<div class="flex items-start gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
259-
<div class="flex flex-wrap items-center gap-3 min-w-0 flex-1">
258+
<div class="flex items-start px-4 py-2 gap-3 compact:py-0.5 compact:gap-2 border-b border-base-300 bg-base-100">
259+
<div class="flex flex-wrap items-center min-w-0 flex-1 gap-3 compact:gap-2">
260260
<FilterToolbar
261261
groups={filterGroups()}
262262
values={viewState.tabFilters.issues}
@@ -354,7 +354,7 @@ export default function IssuesTab(props: IssuesTabProps) {
354354
<button
355355
onClick={() => toggleExpandedRepo("issues", repoGroup.repoFullName)}
356356
aria-expanded={isExpanded()}
357-
class="flex-1 flex items-center gap-2 px-4 py-2.5 text-left text-sm font-semibold text-base-content"
357+
class="flex-1 flex items-center gap-2 px-4 py-2.5 compact:py-1.5 text-left text-sm font-semibold text-base-content"
358358
>
359359
<ChevronIcon size="md" rotated={!isExpanded()} />
360360
{repoGroup.repoFullName}
@@ -411,7 +411,6 @@ export default function IssuesTab(props: IssuesTabProps) {
411411
onIgnore={() => handleIgnore(issue)}
412412
onTrack={config.enableTracking ? () => handleTrack(issue) : undefined}
413413
isTracked={config.enableTracking ? trackedIssueIds().has(issue.id) : undefined}
414-
density={config.viewDensity}
415414
commentCount={issue.comments}
416415
surfacedByBadge={
417416
props.trackedUsers && props.trackedUsers.length > 0

0 commit comments

Comments
 (0)