7771155c8f
* feat(task): add per-group minimize state to dockview store Adds `minimizedGroupIds: Set<string>` plus `toggleGroupMinimized` and `clearMinimizedGroups` actions. The set is cleared on maximize, exit-maximize, preset apply, custom layout apply, env switch, default-layout rebuild, and release-to-default so minimize state never leaks across task environments and never overlaps with maximize. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(task): add minimize button to dockview group header Adds an IconMinus toggle button alongside Maximize/Split/Close in each non-chat dockview group header. Clicking it adds the group ID to the store's `minimizedGroupIds`; a new `useMinimizedGroupSync` hook walks `api.groups`, stamps a `data-kandev-minimized` attribute on each group element, and calls `group.api.setSize` to shrink the group to the splitview minimum (the freed space redistributes to siblings). A CSS rule hides the content container so only the tab strip remains visible. The hook also auto-restores a minimized group when a new panel is added to it (e.g. via the `+` menu) and evicts removed groups from the set. Clicking Minimize while maximized first exits maximize so the user doesn't end up with a header-only view. In wide mode the button is inline; in narrow mode it folds into the existing overflow dropdown as its first item. Chat group never sees the button (same exclusion as Close). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(task): tighten minimize-icon test, drop unused TID The previous "renders Restore tooltip and icon swap" case had two weak assertions: `getAttribute(...) !== null || btn !== null` is tautologically true, and `btn.innerHTML.toContain("svg")` matches both icon variants. Replaced with two focused tests that assert the Tabler class differs between the minimized and non-minimized state. Also dropped the unused `minimizeMenuItem` entry from the TID map. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task): minimize-from-maximize used stale group ID `handleMinimize` previously called `toggleGroupMinimized(group.id)` after `exitMaximizedLayout()`. During maximize, `group.id` is the ephemeral overlay group's ID — once the original layout is restored that ID is gone from `api.groups`, and the minimize was silently lost. Read `maximizedGroupId` (the pre-maximize group ID stored at maximize time) from the store and target that instead. Reported on PR #1039 by claude-review, CodeRabbit, and Greptile. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * perf(task): skip redundant data-attribute writes in minimize sync The sync hook re-runs on every `onDidAddGroup` / `onDidAddPanel`, which previously rewrote `data-kandev-minimized` on every group regardless of whether the value actually changed. Guard with a getAttribute-before-set check. Reported on PR #1039 by claude-review. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(task): cover useMinimizedGroupSync hook Adds 11 unit tests for the hook: mount-time attribute stamping, null-api no-op, minimize/restore round-trip with prior-size replay, auto-restore on panel add into a minimized group, no auto-restore for unrelated groups, group-removal eviction from the store and prior-size map, fresh size on re-added group with the same ID, sync of newly added groups, the perf guard on redundant attribute writes, and clean unmount. Reported on PR #1039 by claude-review as a blocker (CLAUDE.md mandates tests for new hooks). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task): defer minimize toggle to next RAF when exiting maximize `exitMaximizedLayout` schedules `api.layout(w, h)` in a requestAnimation Frame after the synchronous fromJSON rebuild. Calling `toggleGroup Minimized` immediately afterwards races with that layout pass, and the resulting `setSize({ width: 0, height: 0 })` could be overridden — the content was still hidden by CSS but the group kept its full splitview footprint. Defer the toggle one frame so the maximize-exit settles first. Reported on PR #1039 by claude-review, Greptile, and cubic. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task): guard deferred minimize against env-switch + redundant setSize Two small polish fixes from PR #1039 review: - The RAF callback in `handleMinimize` now verifies the target group still exists in `api.groups` before toggling. Within the frame, an env-switch (or another maximize) could clear `minimizedGroupIds`; blindly re-adding a stale ID would leave it in the set, and if the next env happens to reuse the same well-known constant ID (e.g. "right-top"), the wrong group would render as minimized. (cubic P2 finding.) - `applyGroupMinimized` now early-returns when re-syncing an already- minimized group (`priorSizes.has(groupId)`), instead of re-emitting setSize(0,0) and its size-change events. New test covers this. (claude-review suggestion.) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
import type { DockviewApi } from "dockview-react";
|
|
import { useDockviewStore } from "@/lib/state/dockview-store";
|
|
|
|
const MINIMIZED_ATTR = "data-kandev-minimized";
|
|
/** Minimum splitview size we request when minimizing — the splitview clamps to the
|
|
* group's actual minimum (tab-strip height / minimum group width) so this can be 0. */
|
|
const MIN_SIZE = 0;
|
|
|
|
type PriorSize = { width: number; height: number };
|
|
|
|
/** Apply a single group's minimized state: data-attribute + splitview size shrink/restore. */
|
|
function applyGroupMinimized(
|
|
api: DockviewApi,
|
|
groupId: string,
|
|
minimized: boolean,
|
|
priorSizes: Map<string, PriorSize>,
|
|
): void {
|
|
const group = api.groups.find((g) => g.id === groupId);
|
|
if (!group) return;
|
|
const next = minimized ? "true" : "false";
|
|
if (group.element.getAttribute(MINIMIZED_ATTR) !== next) {
|
|
group.element.setAttribute(MINIMIZED_ATTR, next);
|
|
}
|
|
if (minimized) {
|
|
// Skip the setSize when the group is already minimized (priorSizes already
|
|
// has the entry). Re-firing setSize on every sync would re-emit dockview's
|
|
// size-change events for no benefit.
|
|
if (priorSizes.has(groupId)) return;
|
|
priorSizes.set(groupId, { width: group.api.width, height: group.api.height });
|
|
try {
|
|
group.api.setSize({ width: MIN_SIZE, height: MIN_SIZE });
|
|
} catch {
|
|
/* setSize may throw if the group is being torn down */
|
|
}
|
|
} else {
|
|
const prior = priorSizes.get(groupId);
|
|
if (prior) {
|
|
try {
|
|
group.api.setSize({ width: prior.width, height: prior.height });
|
|
} catch {
|
|
/* same */
|
|
}
|
|
priorSizes.delete(groupId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Keep the DOM and splitview sizes in sync with the store's `minimizedGroupIds`.
|
|
*
|
|
* - On set change: walk every group, set the `data-kandev-minimized` attribute,
|
|
* and call `group.api.setSize` to either shrink (clamps to splitview minimum)
|
|
* or restore the prior recorded size.
|
|
* - On `onDidAddPanel` into a minimized group: auto-restore (user is trying to
|
|
* add something they want to see).
|
|
* - On `onDidRemoveGroup`: evict the group from `minimizedGroupIds` and prior-sizes.
|
|
* - On `onDidAddGroup`: new groups default to not-minimized (no-op — they don't
|
|
* appear in the set).
|
|
*/
|
|
export function useMinimizedGroupSync(api: DockviewApi | null): void {
|
|
const priorSizesRef = useRef<Map<string, PriorSize>>(new Map());
|
|
|
|
useEffect(() => {
|
|
if (!api) return;
|
|
const priorSizes = priorSizesRef.current;
|
|
|
|
const sync = () => {
|
|
const ids = useDockviewStore.getState().minimizedGroupIds;
|
|
for (const group of api.groups) {
|
|
applyGroupMinimized(api, group.id, ids.has(group.id), priorSizes);
|
|
}
|
|
};
|
|
|
|
sync();
|
|
|
|
const unsubscribe = useDockviewStore.subscribe((state, prev) => {
|
|
if (state.minimizedGroupIds === prev.minimizedGroupIds) return;
|
|
sync();
|
|
});
|
|
|
|
const d1 = api.onDidAddGroup(() => sync());
|
|
const d2 = api.onDidRemoveGroup((group) => {
|
|
const ids = useDockviewStore.getState().minimizedGroupIds;
|
|
priorSizes.delete(group.id);
|
|
if (ids.has(group.id)) {
|
|
const next = new Set(ids);
|
|
next.delete(group.id);
|
|
useDockviewStore.setState({ minimizedGroupIds: next });
|
|
}
|
|
});
|
|
const d3 = api.onDidAddPanel((panel) => {
|
|
const groupId = panel.group?.id;
|
|
if (!groupId) return;
|
|
const ids = useDockviewStore.getState().minimizedGroupIds;
|
|
if (!ids.has(groupId)) return;
|
|
const next = new Set(ids);
|
|
next.delete(groupId);
|
|
useDockviewStore.setState({ minimizedGroupIds: next });
|
|
});
|
|
|
|
return () => {
|
|
unsubscribe();
|
|
d1.dispose();
|
|
d2.dispose();
|
|
d3.dispose();
|
|
};
|
|
}, [api]);
|
|
}
|