From 9264147cb1b0d8e76d7ea005826778e89b1977da Mon Sep 17 00:00:00 2001 From: 0xPratik Date: Wed, 21 Oct 2026 23:43:19 +0530 Subject: [PATCH 1/7] feat(panel): add a resizable Panel with keep-mounted panes Panel resizes from PanelHandle by pointer without a React render while dragging, by arrow keys, and resets on double-click or Enter. The main area keeps a minimum width and a narrow container gets an overlay. PanelContent with keepMounted keeps a hidden pane's scroll position and typed text. useResizable holds the width logic. --- README.md | 1 + e2e/fixtures.tsx | 77 ++++++- e2e/panel.spec.ts | 218 ++++++++++++++++++++ src/hooks/use-resizable.test.ts | 246 ++++++++++++++++++++++ src/hooks/use-resizable.ts | 340 +++++++++++++++++++++++++++++++ src/index.ts | 2 + src/lib/inert-siblings.test.ts | 50 +++++ src/lib/inert-siblings.ts | 17 ++ src/ui/panel.test.tsx | 220 ++++++++++++++++++++ src/ui/panel.tsx | 348 ++++++++++++++++++++++++++++++++ stories/chat/panel.stories.tsx | 200 ++++++++++++++++++ 11 files changed, 1718 insertions(+), 1 deletion(-) create mode 100644 e2e/panel.spec.ts create mode 100644 src/hooks/use-resizable.test.ts create mode 100644 src/hooks/use-resizable.ts create mode 100644 src/lib/inert-siblings.test.ts create mode 100644 src/lib/inert-siblings.ts create mode 100644 src/ui/panel.test.tsx create mode 100644 src/ui/panel.tsx create mode 100644 stories/chat/panel.stories.tsx diff --git a/README.md b/README.md index da372cb3..c98323da 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Everything imports from `@corbits/react-ui`, except rows marked "optional peer", | `VoiceWaveform` | Live audio level bars | | `DictationStatusLine` | Composer dictation status row | | `MicPermissionDialog` | Microphone permission prompt (optional peer, `ui/mic-permission-dialog`) | +| `Panel` | Side panel resized from its edge; hidden panes keep their state | #### Runs diff --git a/e2e/fixtures.tsx b/e2e/fixtures.tsx index f4b578db..f8da0c5b 100644 --- a/e2e/fixtures.tsx +++ b/e2e/fixtures.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useState } from "react"; +import { useEffect, useLayoutEffect, useState, Profiler } from "react"; import type { ReactNode } from "react"; import { @@ -28,6 +28,13 @@ import { PromptInputTextarea, type PromptInputStatus, type PromptToken, + Panel, + PanelActions, + PanelClose, + PanelContent, + PanelHandle, + PanelHeader, + PanelPane, } from "@corbits/react-ui"; import { CommandPalette, @@ -609,6 +616,73 @@ function PromptWithAttachment() { ); } +/** Counts React commits on `` without rendering anything. */ +function countCommit() { + const body = document.body; + body.dataset["commits"] = String(Number(body.dataset["commits"] ?? 0) + 1); +} + +/** A tall filler, so each pane scrolls. */ +function Filler({ label }: { label: string }) { + return ( +
    + {Array.from({ length: 60 }, (_, index) => ( +
  1. + {label} line {index + 1} +
  2. + ))} +
+ ); +} + +function PanelScenario() { + const [open, setOpen] = useState(true); + const [shown, setShown] = useState("notes"); + const [width, setWidth] = useState(null); + return ( + +
+
+ + + + + {width === null ? "default" : String(width)} + +
+ {open ? ( + + + + + setOpen(false)} /> + + + + + + + + +