feat(ui): Appearance settings — dark/light/system mode and named themes; fix Add lane silently doing nothing (#42)
- Settings → Appearance: mode Dark / Light / System (follows the OS live) and a named theme remembered per mode — dark: Default, Darcula, Apple Dark; light: Default, Apple Light, IntelliJ Light, Canvas. Each theme is a CSS-variable override block keyed by `data-theme` on <html>; the sidebar quick toggle gains System and swaps between the two remembered themes. - An inline boot script in index.html applies the stored mode and theme before first paint, so a reload no longer flashes the default colours. - Diff +/- rows are easier to tell apart on every theme: a /20 status tint, a 3px status bar and a bold coloured marker; the changed-span emphasis no longer vanishes on light themes. - #42: the Add-lane modal (Repo mode / "Current branch") asks POST /api/lanes/ensure with `exact: true`. Without it a folder nested inside an existing lane resolved to that parent lane with created:false, so the modal closed and no new card appeared. When the folder really already has a lane, the modal now says which one instead of closing. Closes #42
This commit is contained in:
@@ -72,3 +72,89 @@ describe("LaneDiffView — side by side", () => {
|
||||
expect(screen.getByTestId("lane-diff-view").className).toContain("overflow-auto");
|
||||
});
|
||||
});
|
||||
|
||||
function fileWith(lines: NonNullable<LaneChangeFile["hunks"]>[number]["lines"]): LaneChangeFile {
|
||||
return { ...FILE, hunks: [{ ...FILE.hunks![0]!, lines }] };
|
||||
}
|
||||
|
||||
const ROWS = fileWith([
|
||||
{ type: "ctx", oldLine: 1, newLine: 1, text: "keep()" },
|
||||
{ type: "del", oldLine: 2, newLine: null, text: "const x = 1;" },
|
||||
{ type: "add", oldLine: null, newLine: 2, text: "const x = 2;" },
|
||||
]);
|
||||
|
||||
/** The unified row whose text contains `needle`. */
|
||||
function rowOf(container: HTMLElement, needle: string): HTMLElement {
|
||||
const row = [...container.querySelectorAll<HTMLElement>("div.flex.w-max")].find((r) =>
|
||||
r.textContent?.includes(needle)
|
||||
);
|
||||
if (!row) throw new Error(`no row for ${needle}`);
|
||||
return row;
|
||||
}
|
||||
|
||||
describe("LaneDiffView — row styling", () => {
|
||||
it("tints add/del rows with their status fill and left bar; context stays plain", () => {
|
||||
const { container } = render(<LaneDiffView file={ROWS} layout="unified" />);
|
||||
const add = rowOf(container, "x = 2");
|
||||
const del = rowOf(container, "x = 1");
|
||||
const ctx = rowOf(container, "keep()");
|
||||
expect(add.className).toContain("bg-status-success/20");
|
||||
expect(add.className).toContain("--status-success");
|
||||
expect(del.className).toContain("bg-status-danger/20");
|
||||
expect(del.className).toContain("--status-danger");
|
||||
expect(ctx.className).not.toMatch(/status-(success|danger)/);
|
||||
});
|
||||
|
||||
it("colours the +/- marker bold in its status colour, context marker muted", () => {
|
||||
const { container } = render(<LaneDiffView file={ROWS} layout="unified" />);
|
||||
const marker = (row: HTMLElement) =>
|
||||
row.querySelector<HTMLElement>("span.select-none:not(.w-10)")!;
|
||||
const plus = marker(rowOf(container, "x = 2"));
|
||||
const minus = marker(rowOf(container, "x = 1"));
|
||||
const ctx = marker(rowOf(container, "keep()"));
|
||||
expect(plus.textContent).toBe("+");
|
||||
expect(plus.className).toContain("font-bold");
|
||||
expect(plus.className).toContain("text-status-success");
|
||||
expect(minus.textContent).toBe("-");
|
||||
expect(minus.className).toContain("text-status-danger");
|
||||
expect(ctx.className).toContain("text-fg-muted");
|
||||
expect(ctx.className).not.toContain("font-bold");
|
||||
});
|
||||
|
||||
it("emphasises only the changed span of a paired del/add", () => {
|
||||
const { container } = render(<LaneDiffView file={ROWS} layout="unified" />);
|
||||
const emph = [...container.querySelectorAll<HTMLElement>("span.bg-fg-primary\\/15")].map(
|
||||
(e) => e.textContent
|
||||
);
|
||||
expect(emph).toEqual(["1", "2"]);
|
||||
});
|
||||
|
||||
it("does not emphasise a line that changed completely", () => {
|
||||
const { container } = render(
|
||||
<LaneDiffView
|
||||
file={fileWith([
|
||||
{ type: "del", oldLine: 1, newLine: null, text: "abc" },
|
||||
{ type: "add", oldLine: null, newLine: 1, text: "xyz" },
|
||||
])}
|
||||
layout="unified"
|
||||
/>
|
||||
);
|
||||
expect(container.querySelector("span.bg-fg-primary\\/15")).toBeNull();
|
||||
});
|
||||
|
||||
it("split view tints each side, and a filler row opposite an unpaired add stays plain", () => {
|
||||
render(
|
||||
<LaneDiffView
|
||||
file={fileWith([{ type: "add", oldLine: null, newLine: 1, text: "added()" }])}
|
||||
layout="split"
|
||||
/>
|
||||
);
|
||||
const rows = (id: string) => [
|
||||
...screen.getByTestId(id).querySelectorAll<HTMLElement>("div.flex.w-max"),
|
||||
];
|
||||
const [right] = rows("lane-diff-split-right");
|
||||
const [left] = rows("lane-diff-split-left");
|
||||
expect(right!.className).toContain("bg-status-success/20");
|
||||
expect(left!.className).not.toMatch(/status-(success|danger)/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user