mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
Sidebar:
- Pinned items: StatusIcon for issues, emoji for projects, sm size, mask gradient text fade
- Pinned items: inline X close button (hidden → flex on hover, desktop tab pattern)
- Pinned section: collapsible with chevron + hover count
- Remove unused canvas token
Global components:
- PageHeader: shared component with built-in mobile SidebarTrigger (md:hidden)
- Replace header divs in all 11 dashboard pages with PageHeader
- Remove standalone mobile trigger bar from DashboardLayout
- Tooltip: 200ms delay, remove arrow, popover/border style
- Search dialog: add finalFocus={false}
- SidebarInset: remove shadow-sm
- Button sizing: icon-xs → icon-sm across all non-editor contexts
Issue Detail:
- Simplify breadcrumb to workspace > identifier
- Extract sidebarContent variable shared between ResizablePanel and mobile Sheet
- All sidebar sections collapsible (Properties, Parent issue, Details, Token usage)
- Auto-close sidebar on mobile breakpoint
- Collapsible section headers: text before chevron, !size-3 stroke-[2.5], hover bg
Project Detail:
- Match Issue Detail layout pattern (header inside left ResizablePanel)
- Extract sidebarContent, add mobile Sheet support
- All sidebar sections collapsible (Properties, Progress, Description)
- Header: move three-dot menu to right button group, unified breadcrumb layout
- Auto-close sidebar on mobile breakpoint
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
63 lines
1.7 KiB
TypeScript
63 lines
1.7 KiB
TypeScript
import "@testing-library/jest-dom/vitest";
|
|
|
|
function createMemoryStorage(): Storage {
|
|
const values = new Map<string, string>();
|
|
|
|
return {
|
|
get length() {
|
|
return values.size;
|
|
},
|
|
clear: () => values.clear(),
|
|
getItem: (key: string) => values.get(key) ?? null,
|
|
key: (index: number) => Array.from(values.keys())[index] ?? null,
|
|
removeItem: (key: string) => {
|
|
values.delete(key);
|
|
},
|
|
setItem: (key: string, value: string) => {
|
|
values.set(key, value);
|
|
},
|
|
};
|
|
}
|
|
|
|
if (typeof globalThis.localStorage?.clear !== "function") {
|
|
const storage = createMemoryStorage();
|
|
Object.defineProperty(globalThis, "localStorage", {
|
|
configurable: true,
|
|
value: storage,
|
|
});
|
|
Object.defineProperty(window, "localStorage", {
|
|
configurable: true,
|
|
value: storage,
|
|
});
|
|
}
|
|
|
|
// jsdom doesn't provide matchMedia; useIsMobile() relies on it.
|
|
if (typeof window.matchMedia !== "function") {
|
|
window.matchMedia = (query: string) =>
|
|
({
|
|
matches: false,
|
|
media: query,
|
|
onchange: null,
|
|
addListener: () => {},
|
|
removeListener: () => {},
|
|
addEventListener: () => {},
|
|
removeEventListener: () => {},
|
|
dispatchEvent: () => false,
|
|
}) as MediaQueryList;
|
|
}
|
|
|
|
// jsdom doesn't provide ResizeObserver; stub it so components that rely on it
|
|
// (e.g. input-otp) can render in tests.
|
|
if (typeof globalThis.ResizeObserver === "undefined") {
|
|
globalThis.ResizeObserver = class ResizeObserver {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
} as unknown as typeof ResizeObserver;
|
|
}
|
|
|
|
// jsdom doesn't implement elementFromPoint; input-otp uses it internally.
|
|
if (typeof document.elementFromPoint !== "function") {
|
|
document.elementFromPoint = () => null;
|
|
}
|