Recovery: re-land PR tab spacing tokens onto main
This commit is contained in:
@@ -968,6 +968,8 @@ The dashboard's CSS is split into a global stylesheet (`packages/dashboard/app/s
|
||||
|
||||
**Rule:** New CSS for a component goes in `app/components/ComponentName.css`, NOT `styles.css`. Only design tokens, primitives (`.btn`, `.card`, `.modal`, `.form-input`), and cross-component `@media` overrides belong in the global file.
|
||||
|
||||
PR tab note: `PrPanel` cards use tokenized `.pr-card` grid spacing (`padding` + `gap`) and boxed token-based hint callouts for empty/loading states.
|
||||
|
||||
The `index.html` shell is templated server-side: the server injects a per-user `<link rel="modulepreload">` for the last-used `taskView` chunk, sourced from Vite's `dist/client/.vite/manifest.json` and `kb:<projectId>:kb-dashboard-task-view` in localStorage.
|
||||
|
||||
### Design tokens
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { PrPanel } from "../PrPanel";
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
refreshPrStatus: vi.fn(),
|
||||
fetchPrChecks: vi.fn().mockResolvedValue({ checks: [], rollup: "unknown", lastCheckedAt: null }),
|
||||
fetchPrReviews: vi.fn().mockResolvedValue({ snapshot: { decision: null, items: [] }, comments: [] }),
|
||||
mergePr: vi.fn(),
|
||||
reclaimPrConflict: vi.fn(),
|
||||
setAutoMergeOnGreen: vi.fn(),
|
||||
unlinkPr: vi.fn(),
|
||||
}));
|
||||
|
||||
const stylesPath = path.resolve(__dirname, "../../styles.css");
|
||||
const stylesSource = fs.readFileSync(stylesPath, "utf-8");
|
||||
|
||||
const baseProps = {
|
||||
taskId: "FN-5596",
|
||||
prAuthAvailable: true,
|
||||
onPrUpdated: vi.fn(),
|
||||
addToast: vi.fn(),
|
||||
};
|
||||
|
||||
const prInfo = {
|
||||
url: "https://github.com/owner/repo/pull/42",
|
||||
number: 42,
|
||||
status: "open" as const,
|
||||
title: "Tokenized spacing",
|
||||
headBranch: "fusion/fn-5596",
|
||||
baseBranch: "main",
|
||||
commentCount: 1,
|
||||
};
|
||||
|
||||
describe("PrPanel spacing contract", () => {
|
||||
it("declares .pr-card tokenized padding and gap in styles.css", () => {
|
||||
expect(stylesSource).toContain(".pr-card {");
|
||||
const start = stylesSource.indexOf(".pr-card {");
|
||||
const block = stylesSource.slice(start, start + 260);
|
||||
expect(block).toContain("padding: var(--space-md);");
|
||||
expect(block).toContain("gap: var(--space-sm);");
|
||||
});
|
||||
|
||||
it("keeps px font-size literals out of the PR section block", () => {
|
||||
const prSectionStart = stylesSource.indexOf("/* === PR Section === */");
|
||||
const nextSectionStart = stylesSource.indexOf("/* ===", prSectionStart + 1);
|
||||
const prSectionBlock = stylesSource.slice(prSectionStart, nextSectionStart);
|
||||
expect(prSectionBlock).not.toContain("font-size: 12px;");
|
||||
expect(prSectionBlock).not.toContain("font-size: 13px;");
|
||||
expect(prSectionBlock).not.toContain("font-size: 14px;");
|
||||
expect(prSectionBlock).not.toContain("font-size: 16px;");
|
||||
});
|
||||
|
||||
it("renders create PR empty-state with subtle hint callout", () => {
|
||||
render(<PrPanel {...baseProps} onRequestCreatePr={vi.fn()} isManualPrFlow />);
|
||||
expect(screen.getByRole("button", { name: "Create PR" })).toBeInTheDocument();
|
||||
expect(document.querySelector(".pr-section .pr-hint--subtle")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("renders multi-PR summary and stacked cards", () => {
|
||||
render(
|
||||
<PrPanel
|
||||
{...baseProps}
|
||||
prInfos={[prInfo, { ...prInfo, number: 43, title: "Second PR" }]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(document.querySelector(".pr-panel-summary")).not.toBeNull();
|
||||
const stack = document.querySelector(".pr-panel-stack");
|
||||
expect(stack).not.toBeNull();
|
||||
expect(stack?.querySelectorAll(".pr-card")).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -1923,24 +1923,31 @@ input[type="range"]:focus-visible {
|
||||
|
||||
/* === PR Section === */
|
||||
.pr-section-icon {
|
||||
vertical-align: middle;
|
||||
margin-right: var(--space-sm);
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.pr-hint--muted {
|
||||
opacity: 0.8;
|
||||
font-size: 13px;
|
||||
font-size: 0.8125rem;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--text-muted) 8%, transparent);
|
||||
}
|
||||
|
||||
.pr-hint--subtle {
|
||||
margin-top: var(--space-sm);
|
||||
opacity: 0.7;
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--text-muted) 8%, transparent);
|
||||
}
|
||||
|
||||
.pr-card--status {
|
||||
border: 1px solid var(--border);
|
||||
.pr-card {
|
||||
border: var(--btn-border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
display: grid;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
@@ -1948,17 +1955,18 @@ input[type="range"]:focus-visible {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pr-status-icon {
|
||||
font-size: 16px;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.pr-status-badge {
|
||||
padding: 2px var(--space-sm);
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 12px;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@@ -2073,8 +2081,8 @@ input[type="range"]:focus-visible {
|
||||
}
|
||||
|
||||
.pr-number {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.pr-spacer {
|
||||
@@ -2086,50 +2094,59 @@ input[type="range"]:focus-visible {
|
||||
}
|
||||
|
||||
.pr-title {
|
||||
color: var(--text);
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.pr-meta {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.pr-meta-arrow {
|
||||
margin: 0 var(--space-sm);
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
font-size: 0.75rem;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.pr-hint--info {
|
||||
margin-bottom: var(--space-sm);
|
||||
font-size: 12px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.pr-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-md);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pr-comments {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
font-size: 0.75rem;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.pr-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-info);
|
||||
display: flex;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
gap: var(--space-xs);
|
||||
color: var(--todo);
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pr-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pr-card {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* === Merge Details Card === */
|
||||
.merge-details-card {
|
||||
border: 1px solid var(--border);
|
||||
|
||||
Reference in New Issue
Block a user