release: publish v2.0.20 selection and sidebar refinements

Complete zero-width sidebar collapse with synchronized bidirectional motion, add package-row selection and guarded double-click disclosure, render continuous selection edges across nested package boundaries, expand regression coverage, and update the public English changelog and package metadata for v2.0.20.
This commit is contained in:
Sucukdeluxe
2026-08-11 04:42:14 +02:00
parent 14090011e3
commit eda3a3f744
10 changed files with 274 additions and 25 deletions
+41
View File
@@ -115,6 +115,47 @@ describe("desktop shell", () => {
expect(html).not.toContain("data-ui-region=\"footer\"");
});
it("collapses a populated sidebar to zero width while preserving the edge toggle", () => {
const html = renderToStaticMarkup(
<AppShell
activeView="downloads"
onViewChange={() => {}}
sidebar={<div>Filter</div>}
sidebarStatus={<div>2 Downloads</div>}
headerActions={null}
toolbar={null}
footer={null}
contextInfo={null}
sidebarCollapsed
onSidebarCollapsedChange={() => {}}
>
<div>Downloads</div>
</AppShell>
);
const shellCss = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
expect(html).toContain("has-collapsed-sidebar");
expect(html).toContain("md-shell-sidebar-panel");
expect(html).toContain("Seitenleiste ausklappen");
expect(shellCss).toMatch(/\.md-shell\.has-collapsed-sidebar \.md-shell-workspace\s*\{[^}]*grid-template-columns:\s*0 minmax\(0,\s*1fr\);/s);
});
it("slides the complete sidebar panel in both directions with the workspace width", () => {
const shellCss = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
expect(shellCss).toMatch(/\.md-shell-workspace\s*\{[^}]*transition:\s*grid-template-columns 520ms cubic-bezier\(0\.22, 0\.76, 0\.22, 1\);/s);
expect(shellCss).toMatch(/\.md-shell-sidebar-panel\s*\{[^}]*transform:\s*translate3d\(0, 0, 0\);[^}]*transition:[^}]*transform 520ms cubic-bezier\(0\.22, 0\.76, 0\.22, 1\)/s);
expect(shellCss).toMatch(/\.md-shell-sidebar\.is-collapsed \.md-shell-sidebar-panel\s*\{[^}]*transform:\s*translate3d\(calc\(-100% - 8px\), 0, 0\);[^}]*pointer-events:\s*none;/s);
expect(shellCss).toMatch(/\.md-shell-sidebar:hover \.md-shell-sidebar-toggle,\s*\.md-shell-sidebar\.is-collapsed \.md-shell-sidebar-toggle,\s*\.md-shell-sidebar-toggle:focus-visible\s*\{[^}]*opacity:\s*1;/s);
});
it("keeps the explicitly requested sidebar motion active when Windows animations are disabled", () => {
const shellCss = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
expect(shellCss).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.md-shell-workspace\s*\{[^}]*transition-duration:\s*520ms !important;/s);
expect(shellCss).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.md-shell-sidebar-panel\s*\{[^}]*transition-duration:\s*260ms, 520ms, 0s !important;/s);
});
it("keeps every view sidebar on the same content axis", () => {
const shellCss = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8").replaceAll("\r\n", "\n");
+117 -5
View File
@@ -709,7 +709,11 @@ describe("downloads view", () => {
const css = readFileSync(new URL("../src/renderer/views/downloads/downloads.css", import.meta.url), "utf8");
const source = readFileSync(new URL("../src/renderer/views/downloads/DownloadsTable.tsx", import.meta.url), "utf8");
expect(css).toMatch(/\.downloads-item-row\.is-selected,\s*\.downloads-package-card\.is-selected\s*>\s*\.downloads-package-row\s*\{[^}]*background:\s*color-mix\(in srgb, var\(--ui-success\) 14%, var\(--ui-canvas\)\);[^}]*box-shadow:\s*inset 3px 0 0 var\(--ui-success\);/s);
expect(css).toMatch(/\.downloads-item-row,\s*\.downloads-package-row\s*\{[^}]*border-left:\s*3px solid transparent;/s);
expect(css).toMatch(/\.downloads-item-row\.is-selected,\s*\.downloads-package-card\.is-selected\s*>\s*\.downloads-package-row\s*\{[^}]*background:\s*color-mix\(in srgb, var\(--ui-success\) 14%, var\(--ui-canvas\)\);[^}]*border-left-color:\s*var\(--ui-success\);/s);
expect(css).toMatch(/\.downloads-item-row\.is-selected::before\s*\{[^}]*content:\s*"";[^}]*position:\s*absolute;[^}]*inset:\s*-1px auto -1px -3px;[^}]*width:\s*3px;[^}]*background:\s*var\(--ui-success\);/s);
expect(css).toMatch(/\.downloads-package-card\.is-selected::before\s*\{[^}]*content:\s*"";[^}]*position:\s*absolute;[^}]*inset:\s*0 auto auto 0;[^}]*width:\s*3px;[^}]*height:\s*41px;[^}]*background:\s*var\(--ui-success\);/s);
expect(css).toMatch(/\.downloads-package-card:has\(\.downloads-package-items-inner\s*>\s*\.downloads-item-row:last-child\.is-selected\)::after\s*\{[^}]*content:\s*"";[^}]*position:\s*absolute;[^}]*inset:\s*auto auto -1px 0;[^}]*width:\s*3px;[^}]*height:\s*1px;[^}]*background:\s*var\(--ui-success\);/s);
expect(css).toMatch(/\.downloads-selection-cell\s+input\[type="checkbox"\]\s*\{[^}]*width:\s*18px;[^}]*height:\s*18px;/s);
expect(css).toMatch(/\.downloads-hoster-icon\s*\{[^}]*width:\s*18px;[^}]*height:\s*18px;[^}]*object-fit:\s*contain;/s);
expect(css).toMatch(/\.downloads-hoster-icon\[data-hoster="rapidgator"\]\s*\{[^}]*transform:\s*translateY\(-4px\) scale\(2\);/s);
@@ -996,12 +1000,39 @@ describe("download table row contracts", () => {
expect(calls).toEqual(["select"]);
});
it("only collapses a package through its visible disclosure button", () => {
it("selects a package on a plain row click", () => {
const calls: Array<[string, boolean, boolean]> = [];
const model = withRuntime(createInput());
const row = model.packageRows[0];
const component = PackageCardContent({
actions: createActions({
onToggleSelection: (id, ctrl, shift) => calls.push([id, ctrl, shift])
}),
columnOrder: model.columnOrder,
editing: false,
editingName: "",
gridTemplate: model.gridTemplate,
packageSpeedBps: 0,
row,
selectedIds: new Set<string>(),
selectedVersion: 1
});
const packageRow = findElement(component, (element) => element.props["data-download-row-id"] === row.package.id);
packageRow.props.onClick({ ctrlKey: false, metaKey: false, shiftKey: false });
expect(calls).toEqual([[row.package.id, false, false]]);
});
it("collapses a package through its disclosure button or a free-row double click", () => {
const calls: string[] = [];
const model = withRuntime(createInput());
const row = model.packageRows[0];
const component = PackageCardContent({
actions: createActions({ onTogglePackageCollapse: () => calls.push("collapse") }),
actions: createActions({
onTogglePackageCollapse: () => calls.push("collapse"),
onToggleSelection: () => calls.push("select")
}),
columnOrder: model.columnOrder,
editing: false,
editingName: "",
@@ -1014,14 +1045,95 @@ describe("download table row contracts", () => {
const packageRow = findElement(component, (element) => element.props["data-download-row-id"] === row.package.id);
const collapseButton = findElement(component, (element) => element.type === "button" && String(element.props.className || "").includes("downloads-collapse-button"));
packageRow.props.onClick({ ctrlKey: false, metaKey: false, shiftKey: false, target: {}, currentTarget: {} });
packageRow.props.onClick({ ctrlKey: false, detail: 1, metaKey: false, shiftKey: false, target: {}, currentTarget: {} });
packageRow.props.onClick({ ctrlKey: false, detail: 2, metaKey: false, shiftKey: false, target: {}, currentTarget: {} });
expect(packageRow.props.onDoubleClick).toBeTypeOf("function");
packageRow.props.onDoubleClick({
ctrlKey: false,
currentTarget: {},
metaKey: false,
preventDefault: () => calls.push("prevent"),
shiftKey: false,
target: { closest: () => null }
});
collapseButton.props.onClick({ stopPropagation: () => {} });
expect(calls).toEqual(["collapse"]);
expect(calls).toEqual(["select", "prevent", "collapse", "collapse"]);
expect(collapseButton.props["aria-expanded"]).toBe(true);
expect(collapseButton.props["aria-controls"]).toBe(`downloads-package-items-${row.package.id}`);
});
it("does not collapse a package when a row double click starts on interactive content", () => {
const calls: string[] = [];
const model = withRuntime(createInput());
const row = model.packageRows[0];
const component = PackageCardContent({
actions: createActions({
onStartPackageRename: () => calls.push("rename"),
onTogglePackageCollapse: () => calls.push("collapse"),
onToggleSelection: () => calls.push("select")
}),
columnOrder: model.columnOrder,
editing: false,
editingName: "",
gridTemplate: model.gridTemplate,
packageSpeedBps: 0,
row,
selectedIds: new Set<string>(),
selectedVersion: 1
});
const packageRow = findElement(component, (element) => element.props["data-download-row-id"] === row.package.id);
const title = findElement(component, (element) => element.type === "strong" && String(element.props.className || "").includes("downloads-copyable"));
packageRow.props.onClick({ ctrlKey: false, detail: 1, metaKey: false, shiftKey: false });
packageRow.props.onClick({ ctrlKey: false, detail: 2, metaKey: false, shiftKey: false });
expect(packageRow.props.onDoubleClick).toBeTypeOf("function");
packageRow.props.onDoubleClick({
ctrlKey: false,
currentTarget: {},
metaKey: false,
preventDefault: () => calls.push("prevent"),
shiftKey: false,
target: { closest: () => ({}) }
});
title.props.onDoubleClick({ stopPropagation: () => calls.push("stop") });
expect(calls).toEqual(["select", "stop", "rename"]);
});
it("keeps an already selected package selected during a row double click", () => {
const calls: string[] = [];
const model = withRuntime(createInput());
const row = model.packageRows[0];
const component = PackageCardContent({
actions: createActions({
onTogglePackageCollapse: () => calls.push("collapse"),
onToggleSelection: () => calls.push("select")
}),
columnOrder: model.columnOrder,
editing: false,
editingName: "",
gridTemplate: model.gridTemplate,
packageSpeedBps: 0,
row,
selectedIds: new Set([row.package.id]),
selectedVersion: 1
});
const packageRow = findElement(component, (element) => element.props["data-download-row-id"] === row.package.id);
packageRow.props.onClick({ ctrlKey: false, detail: 1, metaKey: false, shiftKey: false });
packageRow.props.onClick({ ctrlKey: false, detail: 2, metaKey: false, shiftKey: false });
packageRow.props.onDoubleClick({
ctrlKey: false,
metaKey: false,
preventDefault: () => calls.push("prevent"),
shiftKey: false,
target: { closest: () => null }
});
expect(calls).toEqual(["prevent", "collapse"]);
});
it("keeps the complete package status and audio details in the tooltip", () => {
const audioPackage = {
...pkg("audio-package", "Audio package", ["audio-item"]),
+1 -1
View File
@@ -46,7 +46,7 @@ describe("responsive shell mode", () => {
expect(html).toContain("md-shell is-full");
expect(css).toContain(".md-shell.is-compact");
expect(css).toContain(".md-shell.is-minimum");
expect(css).toMatch(/grid-template-columns:\s*56px minmax\(0,\s*1fr\)/);
expect(css).toMatch(/\.md-shell\.has-collapsed-sidebar \.md-shell-workspace\s*\{[^}]*grid-template-columns:\s*0 minmax\(0,\s*1fr\);/s);
});
it("keeps responsive rail content hidden behind a visible expand control", () => {