mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-11 14:58:39 +03:00
refactor(webui): separate sidebar tabs from panes
This commit is contained in:
+11
-2
@@ -2336,9 +2336,18 @@ function Shell({
|
||||
setWorkbenchState((current) => promoteWorkbenchPane(current, tabKey, paneKey));
|
||||
}, []);
|
||||
|
||||
const onAttachWorkbenchPane = useCallback((paneKey: string, tabKey: string) => {
|
||||
const onAttachWorkbenchPane = useCallback((
|
||||
paneKey: string,
|
||||
tabKey: string,
|
||||
beforePaneKey?: string | null,
|
||||
) => {
|
||||
if (paneKey === tabKey) return;
|
||||
setWorkbenchState((current) => attachWorkbenchPane(current, tabKey, paneKey));
|
||||
setWorkbenchState((current) => attachWorkbenchPane(
|
||||
current,
|
||||
tabKey,
|
||||
paneKey,
|
||||
beforePaneKey,
|
||||
));
|
||||
if (activeKey === paneKey) {
|
||||
navigate({
|
||||
view: "chat",
|
||||
|
||||
+509
-230
File diff suppressed because it is too large
Load Diff
@@ -53,7 +53,11 @@ interface SidebarProps {
|
||||
onPromotePane?: (tabKey: string, paneKey: string) => void;
|
||||
attachableTabKeys?: string[];
|
||||
paneAcceptingTabKeys?: string[];
|
||||
onAttachPane?: (paneKey: string, tabKey: string) => void;
|
||||
onAttachPane?: (
|
||||
paneKey: string,
|
||||
tabKey: string,
|
||||
beforePaneKey?: string | null,
|
||||
) => void;
|
||||
onReorderSessions: (keys: string[]) => void;
|
||||
onToggleGroup: (groupId: string) => void;
|
||||
onRequestRenameProject: (projectKey: string, label: string) => void;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { DraggedPane } from "@/lib/session-drag";
|
||||
|
||||
export interface PaneDropSlot {
|
||||
beforePaneKey: string | null;
|
||||
tabKey: string;
|
||||
}
|
||||
|
||||
export interface PaneTabDragState {
|
||||
height: number;
|
||||
item: DraggedPane;
|
||||
origin: "pane" | "tab";
|
||||
slot: PaneDropSlot | null;
|
||||
}
|
||||
|
||||
export interface PaneTabDragLayout {
|
||||
offsets: Map<string, number>;
|
||||
slotIndex: number;
|
||||
}
|
||||
|
||||
export function samePaneDropSlot(
|
||||
current: PaneDropSlot | null,
|
||||
next: PaneDropSlot | null,
|
||||
): boolean {
|
||||
return current?.tabKey === next?.tabKey
|
||||
&& current?.beforePaneKey === next?.beforePaneKey;
|
||||
}
|
||||
|
||||
export function paneDropSlotForRow(
|
||||
tabKey: string,
|
||||
paneKeys: string[],
|
||||
draggedPaneKey: string,
|
||||
targetPaneKey: string,
|
||||
edge: "before" | "after",
|
||||
): PaneDropSlot {
|
||||
const remaining = paneKeys.filter((key) => key !== draggedPaneKey);
|
||||
const targetIndex = remaining.indexOf(targetPaneKey);
|
||||
const insertionIndex = targetIndex < 0
|
||||
? remaining.length
|
||||
: targetIndex + (edge === "after" ? 1 : 0);
|
||||
return {
|
||||
tabKey,
|
||||
beforePaneKey: remaining[insertionIndex] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function paneTabDragLayout(
|
||||
paneKeys: string[],
|
||||
tabKey: string,
|
||||
drag: PaneTabDragState | null,
|
||||
): PaneTabDragLayout {
|
||||
const offsets = new Map<string, number>();
|
||||
if (!drag || drag.height <= 0) {
|
||||
return { offsets, slotIndex: -1 };
|
||||
}
|
||||
const distance = drag.height + 2;
|
||||
const sourceIndex = paneKeys.indexOf(drag.item.paneKey);
|
||||
if (
|
||||
sourceIndex < 0
|
||||
|| drag.item.sourceTabKey !== tabKey
|
||||
|| drag.slot?.tabKey !== tabKey
|
||||
) {
|
||||
return { offsets, slotIndex: -1 };
|
||||
}
|
||||
|
||||
const remaining = paneKeys.filter((key) => key !== drag.item.paneKey);
|
||||
const requestedIndex = drag.slot.beforePaneKey
|
||||
? remaining.indexOf(drag.slot.beforePaneKey)
|
||||
: remaining.length;
|
||||
const slotIndex = requestedIndex < 0 ? remaining.length : requestedIndex;
|
||||
|
||||
if (sourceIndex < slotIndex) {
|
||||
for (let index = sourceIndex + 1; index <= slotIndex; index += 1) {
|
||||
offsets.set(paneKeys[index], -distance);
|
||||
}
|
||||
} else if (sourceIndex > slotIndex) {
|
||||
for (let index = slotIndex; index < sourceIndex; index += 1) {
|
||||
offsets.set(paneKeys[index], distance);
|
||||
}
|
||||
}
|
||||
return { offsets, slotIndex };
|
||||
}
|
||||
@@ -39,6 +39,19 @@ function uniqueKeys(value: unknown): string[] {
|
||||
));
|
||||
}
|
||||
|
||||
function insertPaneBefore(
|
||||
paneKeys: string[],
|
||||
paneKey: string,
|
||||
beforePaneKey?: string | null,
|
||||
): string[] {
|
||||
const next = paneKeys.filter((key) => key !== paneKey);
|
||||
const requestedIndex = beforePaneKey && beforePaneKey !== paneKey
|
||||
? next.indexOf(beforePaneKey)
|
||||
: -1;
|
||||
next.splice(requestedIndex < 0 ? next.length : requestedIndex, 0, paneKey);
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizeTab(value: unknown, tabKey: string): WorkbenchTabState {
|
||||
const candidate = value && typeof value === "object"
|
||||
? value as Partial<WorkbenchTabState>
|
||||
@@ -170,6 +183,7 @@ export function attachWorkbenchPane(
|
||||
state: WorkbenchState,
|
||||
targetTabKey: string,
|
||||
paneKey: string,
|
||||
beforePaneKey?: string | null,
|
||||
): WorkbenchState {
|
||||
if (!targetTabKey || !paneKey || targetTabKey === paneKey) return state;
|
||||
|
||||
@@ -179,7 +193,19 @@ export function attachWorkbenchPane(
|
||||
const sourceTabKey = sourceEntry?.[0];
|
||||
const sourceTab = sourceEntry?.[1];
|
||||
if (sourceTabKey === targetTabKey) {
|
||||
return focusWorkbenchPane(state, targetTabKey, paneKey);
|
||||
if (beforePaneKey === undefined) {
|
||||
return focusWorkbenchPane(state, targetTabKey, paneKey);
|
||||
}
|
||||
if (!sourceTab) return state;
|
||||
const paneKeys = insertPaneBefore(sourceTab.paneKeys, paneKey, beforePaneKey);
|
||||
if (paneKeys.every((key, index) => key === sourceTab.paneKeys[index])) return state;
|
||||
return {
|
||||
version: 2,
|
||||
tabs: {
|
||||
...state.tabs,
|
||||
[targetTabKey]: { ...sourceTab, paneKeys },
|
||||
},
|
||||
};
|
||||
}
|
||||
if (sourceTabKey === paneKey && sourceTab && sourceTab.paneKeys.length > 1) {
|
||||
return state;
|
||||
@@ -210,13 +236,12 @@ export function attachWorkbenchPane(
|
||||
}
|
||||
|
||||
const targetTab = tabs[targetTabKey] ?? defaultWorkbenchTab(targetTabKey);
|
||||
tabs[targetTabKey] = targetTab.paneKeys.includes(paneKey)
|
||||
? { ...targetTab, activePaneKey: paneKey }
|
||||
: {
|
||||
...targetTab,
|
||||
paneKeys: [...targetTab.paneKeys, paneKey],
|
||||
activePaneKey: paneKey,
|
||||
};
|
||||
const paneKeys = insertPaneBefore(targetTab.paneKeys, paneKey, beforePaneKey);
|
||||
tabs[targetTabKey] = {
|
||||
...targetTab,
|
||||
paneKeys,
|
||||
activePaneKey: paneKey,
|
||||
};
|
||||
return { version: 2, tabs };
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
--radius: 0.4375rem;
|
||||
--sidebar: 40 8% 96.8%;
|
||||
--sidebar-foreground: 0 0% 3.9%;
|
||||
--sidebar-selected: 40 1% 89.4%;
|
||||
--sidebar-accent: 0 0% 95.8%;
|
||||
--sidebar-accent-foreground: 0 0% 9%;
|
||||
--sidebar-border: 40 8% 90.5%;
|
||||
@@ -77,6 +78,7 @@
|
||||
--temporary-border: 27 96% 61%;
|
||||
--sidebar: var(--card);
|
||||
--sidebar-foreground: 0 0% 98%;
|
||||
--sidebar-selected: 0 0% 29.8%;
|
||||
--sidebar-accent: var(--background);
|
||||
--sidebar-accent-foreground: 0 0% 98%;
|
||||
--sidebar-border: var(--border);
|
||||
|
||||
@@ -1409,14 +1409,17 @@
|
||||
"workbench": {
|
||||
"aria": "Conversation workbench",
|
||||
"panes": "Panes",
|
||||
"tabAria": "Tab: {{title}}",
|
||||
"panesInTab": "Panes in {{title}}",
|
||||
"collapseTabGroup": "Collapse panes in {{title}}",
|
||||
"expandTabGroup": "Expand panes in {{title}}",
|
||||
"dropPane": "Move {{pane}} into {{tab}}",
|
||||
"moveToTab": "Move to tab",
|
||||
"layout": "Pane layout",
|
||||
"addPane": "Add pane",
|
||||
"promotePane": "Make {{title}} the primary pane",
|
||||
"paneActions": "{{title}} pane actions",
|
||||
"detachPane": "Move {{title}} to its own topic",
|
||||
"detachPane": "Move {{title}} to a new tab",
|
||||
"composerAria": "Message {{title}}",
|
||||
"layouts": {
|
||||
"columns": "Columns",
|
||||
|
||||
@@ -1396,14 +1396,17 @@
|
||||
"workbench": {
|
||||
"aria": "Área de conversaciones",
|
||||
"panes": "Paneles",
|
||||
"tabAria": "Pestaña: {{title}}",
|
||||
"panesInTab": "Paneles de {{title}}",
|
||||
"collapseTabGroup": "Contraer los paneles de {{title}}",
|
||||
"expandTabGroup": "Expandir los paneles de {{title}}",
|
||||
"dropPane": "Mover {{pane}} a {{tab}}",
|
||||
"moveToTab": "Mover a una pestaña",
|
||||
"layout": "Diseño de paneles",
|
||||
"addPane": "Añadir panel",
|
||||
"promotePane": "Convertir {{title}} en el panel principal",
|
||||
"paneActions": "Acciones del panel {{title}}",
|
||||
"detachPane": "Mover {{title}} a su propio tema",
|
||||
"detachPane": "Mover {{title}} a una pestaña nueva",
|
||||
"composerAria": "Mensaje para {{title}}",
|
||||
"layouts": {
|
||||
"columns": "Columnas",
|
||||
|
||||
@@ -1395,14 +1395,17 @@
|
||||
"workbench": {
|
||||
"aria": "Espace de conversations",
|
||||
"panes": "Volets",
|
||||
"tabAria": "Onglet : {{title}}",
|
||||
"panesInTab": "Volets dans {{title}}",
|
||||
"collapseTabGroup": "Réduire les volets de {{title}}",
|
||||
"expandTabGroup": "Développer les volets de {{title}}",
|
||||
"dropPane": "Déplacer {{pane}} dans {{tab}}",
|
||||
"moveToTab": "Déplacer vers un onglet",
|
||||
"layout": "Disposition des volets",
|
||||
"addPane": "Ajouter un volet",
|
||||
"promotePane": "Définir {{title}} comme volet principal",
|
||||
"paneActions": "Actions du volet {{title}}",
|
||||
"detachPane": "Déplacer {{title}} vers son propre sujet",
|
||||
"detachPane": "Déplacer {{title}} vers un nouvel onglet",
|
||||
"composerAria": "Message à {{title}}",
|
||||
"layouts": {
|
||||
"columns": "Colonnes",
|
||||
|
||||
@@ -1395,14 +1395,17 @@
|
||||
"workbench": {
|
||||
"aria": "Ruang kerja percakapan",
|
||||
"panes": "Panel",
|
||||
"tabAria": "Tab: {{title}}",
|
||||
"panesInTab": "Panel di {{title}}",
|
||||
"collapseTabGroup": "Ciutkan panel di {{title}}",
|
||||
"expandTabGroup": "Luaskan panel di {{title}}",
|
||||
"dropPane": "Pindahkan {{pane}} ke {{tab}}",
|
||||
"moveToTab": "Pindahkan ke tab",
|
||||
"layout": "Tata letak panel",
|
||||
"addPane": "Tambah panel",
|
||||
"promotePane": "Jadikan {{title}} panel utama",
|
||||
"paneActions": "Tindakan panel {{title}}",
|
||||
"detachPane": "Pindahkan {{title}} ke topik tersendiri",
|
||||
"detachPane": "Pindahkan {{title}} ke tab baru",
|
||||
"composerAria": "Pesan untuk {{title}}",
|
||||
"layouts": {
|
||||
"columns": "Kolom",
|
||||
|
||||
@@ -1395,14 +1395,17 @@
|
||||
"workbench": {
|
||||
"aria": "会話ワークベンチ",
|
||||
"panes": "ペイン",
|
||||
"tabAria": "タブ:{{title}}",
|
||||
"panesInTab": "{{title}} のペイン",
|
||||
"collapseTabGroup": "{{title}} のペインを折りたたむ",
|
||||
"expandTabGroup": "{{title}} のペインを展開する",
|
||||
"dropPane": "{{pane}} を {{tab}} に移動",
|
||||
"moveToTab": "タブへ移動",
|
||||
"layout": "ペインレイアウト",
|
||||
"addPane": "ペインを追加",
|
||||
"promotePane": "{{title}} をメインペインにする",
|
||||
"paneActions": "{{title}} ペインの操作",
|
||||
"detachPane": "{{title}} を独立したトピックに移動",
|
||||
"detachPane": "{{title}} を新しいタブに移動",
|
||||
"composerAria": "{{title}} へのメッセージ",
|
||||
"layouts": {
|
||||
"columns": "列",
|
||||
|
||||
@@ -1395,14 +1395,17 @@
|
||||
"workbench": {
|
||||
"aria": "대화 워크벤치",
|
||||
"panes": "창",
|
||||
"tabAria": "탭: {{title}}",
|
||||
"panesInTab": "{{title}}의 창",
|
||||
"collapseTabGroup": "{{title}}의 창 접기",
|
||||
"expandTabGroup": "{{title}}의 창 펼치기",
|
||||
"dropPane": "{{pane}}을(를) {{tab}}으로 이동",
|
||||
"moveToTab": "탭으로 이동",
|
||||
"layout": "창 레이아웃",
|
||||
"addPane": "창 추가",
|
||||
"promotePane": "{{title}}을(를) 기본 창으로 설정",
|
||||
"paneActions": "{{title}} 창 작업",
|
||||
"detachPane": "{{title}}을(를) 별도 주제로 이동",
|
||||
"detachPane": "{{title}}을(를) 새 탭으로 이동",
|
||||
"composerAria": "{{title}}에 메시지 보내기",
|
||||
"layouts": {
|
||||
"columns": "열",
|
||||
|
||||
@@ -1409,14 +1409,17 @@
|
||||
"workbench": {
|
||||
"aria": "Área de conversas",
|
||||
"panes": "Painéis",
|
||||
"tabAria": "Aba: {{title}}",
|
||||
"panesInTab": "Painéis em {{title}}",
|
||||
"collapseTabGroup": "Recolher os painéis em {{title}}",
|
||||
"expandTabGroup": "Expandir os painéis em {{title}}",
|
||||
"dropPane": "Mover {{pane}} para {{tab}}",
|
||||
"moveToTab": "Mover para uma aba",
|
||||
"layout": "Layout de painéis",
|
||||
"addPane": "Adicionar painel",
|
||||
"promotePane": "Tornar {{title}} o painel principal",
|
||||
"paneActions": "Ações do painel {{title}}",
|
||||
"detachPane": "Mover {{title}} para seu próprio tópico",
|
||||
"detachPane": "Mover {{title}} para uma nova aba",
|
||||
"composerAria": "Mensagem para {{title}}",
|
||||
"layouts": {
|
||||
"columns": "Colunas",
|
||||
|
||||
@@ -1395,14 +1395,17 @@
|
||||
"workbench": {
|
||||
"aria": "Không gian hội thoại",
|
||||
"panes": "Khung",
|
||||
"tabAria": "Thẻ: {{title}}",
|
||||
"panesInTab": "Các khung trong {{title}}",
|
||||
"collapseTabGroup": "Thu gọn các khung trong {{title}}",
|
||||
"expandTabGroup": "Mở rộng các khung trong {{title}}",
|
||||
"dropPane": "Di chuyển {{pane}} vào {{tab}}",
|
||||
"moveToTab": "Di chuyển vào thẻ",
|
||||
"layout": "Bố cục khung",
|
||||
"addPane": "Thêm khung",
|
||||
"promotePane": "Đặt {{title}} làm khung chính",
|
||||
"paneActions": "Thao tác cho khung {{title}}",
|
||||
"detachPane": "Chuyển {{title}} thành chủ đề riêng",
|
||||
"detachPane": "Chuyển {{title}} sang thẻ mới",
|
||||
"composerAria": "Nhắn tin cho {{title}}",
|
||||
"layouts": {
|
||||
"columns": "Cột",
|
||||
|
||||
@@ -1409,14 +1409,17 @@
|
||||
"workbench": {
|
||||
"aria": "会话工作台",
|
||||
"panes": "窗格",
|
||||
"tabAria": "标签页:{{title}}",
|
||||
"panesInTab": "{{title}} 中的窗格",
|
||||
"collapseTabGroup": "折叠 {{title}} 中的窗格",
|
||||
"expandTabGroup": "展开 {{title}} 中的窗格",
|
||||
"dropPane": "将 {{pane}} 移入 {{tab}}",
|
||||
"moveToTab": "移动到标签页",
|
||||
"layout": "窗格布局",
|
||||
"addPane": "添加窗格",
|
||||
"promotePane": "将 {{title}} 设为主窗格",
|
||||
"paneActions": "{{title}} 窗格操作",
|
||||
"detachPane": "将 {{title}} 移至独立主题",
|
||||
"detachPane": "将 {{title}} 移至新标签页",
|
||||
"composerAria": "向 {{title}} 发送消息",
|
||||
"layouts": {
|
||||
"columns": "列布局",
|
||||
|
||||
@@ -1395,14 +1395,17 @@
|
||||
"workbench": {
|
||||
"aria": "對話工作台",
|
||||
"panes": "窗格",
|
||||
"tabAria": "標籤頁:{{title}}",
|
||||
"panesInTab": "{{title}} 中的窗格",
|
||||
"collapseTabGroup": "收合 {{title}} 中的窗格",
|
||||
"expandTabGroup": "展開 {{title}} 中的窗格",
|
||||
"dropPane": "將 {{pane}} 移入 {{tab}}",
|
||||
"moveToTab": "移動到分頁",
|
||||
"layout": "窗格佈局",
|
||||
"addPane": "新增窗格",
|
||||
"promotePane": "將 {{title}} 設為主窗格",
|
||||
"paneActions": "{{title}} 窗格操作",
|
||||
"detachPane": "將 {{title}} 移至獨立主題",
|
||||
"detachPane": "將 {{title}} 移至新標籤頁",
|
||||
"composerAria": "傳送訊息給 {{title}}",
|
||||
"layouts": {
|
||||
"columns": "欄佈局",
|
||||
|
||||
@@ -3115,7 +3115,7 @@ describe("App layout", () => {
|
||||
name: "New topic pane actions",
|
||||
}), { button: 0, ctrlKey: false });
|
||||
fireEvent.click(screen.getByRole("menuitem", {
|
||||
name: "Move New topic to its own topic",
|
||||
name: "Move New topic to a new tab",
|
||||
}));
|
||||
await waitFor(() => expect(screen.getByTestId("pane-grid").children).toHaveLength(1));
|
||||
expect(within(sidebar).getAllByRole("button", { name: "New topic" })).toHaveLength(2);
|
||||
|
||||
@@ -72,6 +72,7 @@ describe("ChatList", () => {
|
||||
const dataTransfer = {
|
||||
effectAllowed: "",
|
||||
setData: vi.fn(),
|
||||
setDragImage: vi.fn(),
|
||||
};
|
||||
render(
|
||||
<ChatList
|
||||
@@ -99,7 +100,14 @@ describe("ChatList", () => {
|
||||
SESSION_DRAG_TYPE,
|
||||
"websocket:reference",
|
||||
);
|
||||
expect(dataTransfer.setDragImage).toHaveBeenCalled();
|
||||
expect(document.querySelector("[data-pane-drag-overlay]"))
|
||||
.toHaveTextContent("Reference chat");
|
||||
expect(reference.closest("li")).not.toHaveClass("opacity-0");
|
||||
expect(document.querySelector("[data-tab-drag-placeholder]"))
|
||||
.not.toBeInTheDocument();
|
||||
fireEvent.dragEnd(reference, { dataTransfer });
|
||||
expect(document.querySelector("[data-pane-drag-overlay]")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("reorders topics with a displaced-neighbor preview instead of an insertion line", () => {
|
||||
@@ -182,7 +190,7 @@ describe("ChatList", () => {
|
||||
expect(text.indexOf("Charlie")).toBeLessThan(text.indexOf("Alpha"));
|
||||
});
|
||||
|
||||
it("shows every tab's pane membership in the sidebar tree", async () => {
|
||||
it("shows every tab's pane membership in a sidebar tab group", async () => {
|
||||
const onSelect = vi.fn();
|
||||
const onSelectPane = vi.fn();
|
||||
const onDetachPane = vi.fn();
|
||||
@@ -236,7 +244,8 @@ describe("ChatList", () => {
|
||||
expect(child.closest("[data-sidebar-pane]"))
|
||||
.toHaveAttribute("data-sidebar-pane", "websocket:child");
|
||||
expect(child).toHaveAttribute("aria-current", "true");
|
||||
const targetTabRow = screen.getByRole("button", { name: "Target tab" }).closest("li")!;
|
||||
const targetTabRow = screen.getByRole("button", { name: "Tab: Target tab" })
|
||||
.closest("li")!;
|
||||
const targetChild = within(targetTabRow).getByRole("button", {
|
||||
name: "Target research",
|
||||
});
|
||||
@@ -255,6 +264,10 @@ describe("ChatList", () => {
|
||||
expect(onSelectPane).toHaveBeenCalledWith("websocket:root", "websocket:root");
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
|
||||
onSelectPane.mockClear();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Tab: Root topic" }));
|
||||
expect(onSelectPane).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.pointerDown(screen.getByRole("button", {
|
||||
name: "Research pane pane actions",
|
||||
}), { button: 0, ctrlKey: false });
|
||||
@@ -267,25 +280,34 @@ describe("ChatList", () => {
|
||||
name: "Research pane pane actions",
|
||||
}), { button: 0, ctrlKey: false });
|
||||
fireEvent.click(await screen.findByRole("menuitem", {
|
||||
name: "Move Research pane to its own topic",
|
||||
name: "Move Research pane to a new tab",
|
||||
}));
|
||||
expect(onDetachPane).toHaveBeenCalledWith("websocket:root", "websocket:child");
|
||||
|
||||
fireEvent.pointerDown(screen.getByRole("button", {
|
||||
name: "Root topic pane actions",
|
||||
}), { button: 0, ctrlKey: false });
|
||||
expect(screen.queryByRole("menuitem", { name: "Move to tab" }))
|
||||
.not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("menuitem", { name: "Move Root topic to a new tab" }))
|
||||
.not.toBeInTheDocument();
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
|
||||
const dataTransfer = {
|
||||
effectAllowed: "",
|
||||
dropEffect: "",
|
||||
setData: vi.fn(),
|
||||
};
|
||||
onAttachPane.mockClear();
|
||||
fireEvent.dragStart(child, { dataTransfer });
|
||||
expect(child.closest("li")).toHaveAttribute("data-pane-dragging", "true");
|
||||
expect(child.closest("li")).not.toHaveClass("opacity-0");
|
||||
const targetTab = screen.getByRole("button", { name: "Target tab" });
|
||||
const targetTab = screen.getByRole("button", { name: "Tab: Target tab" });
|
||||
dragOverAt(targetTab.closest("li")!, 0, dataTransfer);
|
||||
expect(targetTab.closest("li"))
|
||||
.toHaveAttribute("data-tab-attach-target", "true");
|
||||
expect(within(targetTab.closest("li")!).getByRole("status", {
|
||||
name: "Move Research pane into Target tab",
|
||||
})).toHaveTextContent("Research pane");
|
||||
.not.toHaveAttribute("data-tab-attach-target");
|
||||
expect(targetTab.closest("li")!.querySelector("[data-pane-snap-slot]"))
|
||||
.not.toBeInTheDocument();
|
||||
dropAt(targetTab.closest("li")!, 0, dataTransfer);
|
||||
expect(dataTransfer.setData).toHaveBeenCalledWith(
|
||||
PANE_DRAG_TYPE,
|
||||
@@ -294,7 +316,217 @@ describe("ChatList", () => {
|
||||
sourceTabKey: "websocket:root",
|
||||
}),
|
||||
);
|
||||
expect(onAttachPane).toHaveBeenCalledWith("websocket:child", "websocket:target");
|
||||
expect(onAttachPane).not.toHaveBeenCalled();
|
||||
fireEvent.dragEnd(child, { dataTransfer });
|
||||
});
|
||||
|
||||
it("collapses a multi-pane tab into one Chrome-style group header", () => {
|
||||
render(
|
||||
<ChatList
|
||||
sessions={[session({ chatId: "root", title: "Root topic" })]}
|
||||
activeKey="websocket:root"
|
||||
paneGroups={{
|
||||
"websocket:root": {
|
||||
topicKey: "websocket:root",
|
||||
activePaneKey: "websocket:child",
|
||||
panes: [
|
||||
{ key: "websocket:root", chatId: "root", title: "Root topic" },
|
||||
{ key: "websocket:child", chatId: "child", title: "Research pane" },
|
||||
],
|
||||
},
|
||||
}}
|
||||
onSelect={vi.fn()}
|
||||
onSelectPane={vi.fn()}
|
||||
onRequestDelete={vi.fn()}
|
||||
onTogglePin={vi.fn()}
|
||||
onRequestRename={vi.fn()}
|
||||
onToggleArchive={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tabGroup = screen.getByRole("button", { name: "Tab: Root topic" })
|
||||
.closest("[data-sidebar-tab-group]")!;
|
||||
expect(tabGroup).toHaveAttribute("data-sidebar-tab-group", "true");
|
||||
expect(within(tabGroup).getByRole("list", { name: "Panes in Root topic" }))
|
||||
.toBeInTheDocument();
|
||||
expect(within(tabGroup).getByRole("button", { name: "Research pane" }))
|
||||
.toHaveAttribute("aria-current", "true");
|
||||
expect(within(tabGroup).getByRole("button", { name: "Root topic" }))
|
||||
.not.toHaveAttribute("aria-current");
|
||||
expect(tabGroup).not.toHaveTextContent("2/4");
|
||||
|
||||
const collapse = within(tabGroup).getByRole("button", {
|
||||
name: "Collapse panes in Root topic",
|
||||
});
|
||||
expect(collapse).toHaveAttribute("aria-expanded", "true");
|
||||
fireEvent.click(collapse);
|
||||
|
||||
expect(tabGroup).toHaveAttribute("data-pane-group-collapsed", "true");
|
||||
expect(within(tabGroup).queryByRole("button", { name: "Research pane" }))
|
||||
.not.toBeInTheDocument();
|
||||
expect(within(tabGroup).getByRole("button", {
|
||||
name: "Expand panes in Root topic",
|
||||
})).toHaveAttribute("aria-expanded", "false");
|
||||
expect(within(tabGroup).getByRole("button", { name: "Tab: Root topic" })
|
||||
.closest("[data-sidebar-tab]"))
|
||||
.toHaveClass("bg-sidebar-selected");
|
||||
|
||||
fireEvent.click(within(tabGroup).getByRole("button", {
|
||||
name: "Expand panes in Root topic",
|
||||
}));
|
||||
expect(within(tabGroup).getByRole("button", { name: "Research pane" }))
|
||||
.toBeInTheDocument();
|
||||
expect(within(tabGroup).getByRole("button", { name: "Root topic" }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the Pane opaque without exposing a slot in another tab", () => {
|
||||
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue(rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 240,
|
||||
height: 28,
|
||||
}));
|
||||
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
|
||||
callback(0);
|
||||
return 1;
|
||||
});
|
||||
const dataTransfer = {
|
||||
effectAllowed: "",
|
||||
dropEffect: "",
|
||||
setData: vi.fn(),
|
||||
setDragImage: vi.fn(),
|
||||
};
|
||||
const onAttachPane = vi.fn();
|
||||
|
||||
render(
|
||||
<ChatList
|
||||
sessions={[
|
||||
session({ chatId: "root", title: "Root topic" }),
|
||||
session({ chatId: "target", title: "Target tab" }),
|
||||
]}
|
||||
activeKey="websocket:root"
|
||||
paneGroups={{
|
||||
"websocket:root": {
|
||||
topicKey: "websocket:root",
|
||||
activePaneKey: "websocket:child",
|
||||
panes: [
|
||||
{ key: "websocket:root", chatId: "root", title: "Root topic" },
|
||||
{ key: "websocket:child", chatId: "child", title: "Research pane" },
|
||||
],
|
||||
},
|
||||
}}
|
||||
paneAcceptingTabKeys={["websocket:target"]}
|
||||
onAttachPane={onAttachPane}
|
||||
onSelect={vi.fn()}
|
||||
onRequestDelete={vi.fn()}
|
||||
onTogglePin={vi.fn()}
|
||||
onRequestRename={vi.fn()}
|
||||
onToggleArchive={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const pane = screen.getByRole("button", { name: "Research pane" });
|
||||
fireEvent.dragStart(pane, { clientX: 40, clientY: 40, dataTransfer });
|
||||
expect(dataTransfer.setDragImage).toHaveBeenCalled();
|
||||
|
||||
const dragOver = createEvent.dragOver(
|
||||
screen.getByRole("button", { name: "Target tab" }).closest("li")!,
|
||||
{ dataTransfer },
|
||||
);
|
||||
Object.defineProperties(dragOver, {
|
||||
clientX: { value: 160 },
|
||||
clientY: { value: 120 },
|
||||
});
|
||||
fireEvent(screen.getByRole("button", { name: "Target tab" }).closest("li")!, dragOver);
|
||||
|
||||
const paneRow = pane.closest("li")!;
|
||||
const overlay = document.querySelector<HTMLElement>("[data-pane-drag-overlay]")!;
|
||||
expect(overlay).toHaveStyle({
|
||||
opacity: "1",
|
||||
height: "28px",
|
||||
transform: "translate3d(40px, 106px, 0)",
|
||||
visibility: "visible",
|
||||
width: "240px",
|
||||
});
|
||||
expect(overlay).toHaveTextContent("Research pane");
|
||||
expect(overlay).toHaveClass(
|
||||
"!bg-sidebar-selected",
|
||||
"!shadow-none",
|
||||
);
|
||||
expect(overlay).toHaveStyle({ boxShadow: "none" });
|
||||
expect(document.querySelector("[data-pane-snap-slot]")).not.toBeInTheDocument();
|
||||
expect(paneRow.querySelector("[data-sidebar-pane]"))
|
||||
.toHaveClass("!bg-transparent", "!text-transparent", "!shadow-none");
|
||||
expect(paneRow.style.transform).toBe("");
|
||||
expect(paneRow).not.toHaveClass("opacity-0");
|
||||
|
||||
dataTransfer.dropEffect = "none";
|
||||
fireEvent.dragEnd(pane, { clientX: 160, clientY: 120, dataTransfer });
|
||||
expect(onAttachPane).not.toHaveBeenCalled();
|
||||
expect(document.querySelector("[data-pane-drag-overlay]")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("repels sibling Panes and snaps the dragged Pane into the selected slot", () => {
|
||||
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue(rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 240,
|
||||
height: 28,
|
||||
}));
|
||||
const onAttachPane = vi.fn();
|
||||
const dataTransfer = {
|
||||
effectAllowed: "",
|
||||
dropEffect: "",
|
||||
setData: vi.fn(),
|
||||
setDragImage: vi.fn(),
|
||||
};
|
||||
|
||||
render(
|
||||
<ChatList
|
||||
sessions={[session({ chatId: "root", title: "Root topic" })]}
|
||||
activeKey="websocket:root"
|
||||
paneGroups={{
|
||||
"websocket:root": {
|
||||
topicKey: "websocket:root",
|
||||
activePaneKey: "websocket:first",
|
||||
panes: [
|
||||
{ key: "websocket:root", chatId: "root", title: "Root topic" },
|
||||
{ key: "websocket:first", chatId: "first", title: "First pane" },
|
||||
{ key: "websocket:second", chatId: "second", title: "Second pane" },
|
||||
{ key: "websocket:third", chatId: "third", title: "Third pane" },
|
||||
],
|
||||
},
|
||||
}}
|
||||
onAttachPane={onAttachPane}
|
||||
onSelect={vi.fn()}
|
||||
onRequestDelete={vi.fn()}
|
||||
onTogglePin={vi.fn()}
|
||||
onRequestRename={vi.fn()}
|
||||
onToggleArchive={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const first = screen.getByRole("button", { name: "First pane" });
|
||||
const secondRow = screen.getByRole("button", { name: "Second pane" }).closest("li")!;
|
||||
fireEvent.dragStart(first, { clientX: 40, clientY: 14, dataTransfer });
|
||||
dragOverAt(secondRow, 20, dataTransfer);
|
||||
|
||||
expect(secondRow).toHaveAttribute("data-pane-displaced", "true");
|
||||
expect(secondRow).toHaveStyle("transform: translateY(-30px)");
|
||||
expect((first.closest("li") as HTMLElement).style.transform).toBe("");
|
||||
const snapSlot = screen.getByRole("list", { name: "Panes in Root topic" })
|
||||
.querySelector("[data-pane-snap-slot]")!;
|
||||
expect(snapSlot).toHaveStyle("height: 28px; transform: translateY(60px)");
|
||||
expect(snapSlot).toHaveClass("absolute", "bg-transparent");
|
||||
expect(first.closest("li")).not.toHaveClass("opacity-0");
|
||||
|
||||
dropAt(snapSlot, 20, dataTransfer);
|
||||
expect(onAttachPane).toHaveBeenCalledWith(
|
||||
"websocket:first",
|
||||
"websocket:root",
|
||||
"websocket:third",
|
||||
);
|
||||
});
|
||||
|
||||
it("selects a whole tab or individual panes for one bulk delete", async () => {
|
||||
@@ -330,6 +562,8 @@ describe("ChatList", () => {
|
||||
}), { button: 0, ctrlKey: false });
|
||||
fireEvent.click(await screen.findByRole("menuitem", { name: "Select" }));
|
||||
|
||||
expect(screen.getByRole("button", { name: "Tab: Root topic" }))
|
||||
.toHaveAttribute("aria-pressed", "true");
|
||||
expect(screen.getByRole("button", { name: "Root topic" }))
|
||||
.toHaveAttribute("aria-pressed", "true");
|
||||
expect(screen.getByRole("button", { name: "Research pane" }))
|
||||
@@ -350,7 +584,7 @@ describe("ChatList", () => {
|
||||
expect(screen.queryByTestId("delete-selection-bar")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("reattaches a one-pane tab through the center of another tab", () => {
|
||||
it("reorders one-pane tabs instead of attaching them through drag", () => {
|
||||
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue(rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
@@ -392,16 +626,16 @@ describe("ChatList", () => {
|
||||
.toHaveAttribute("data-session-dragging", "true");
|
||||
const target = screen.getByRole("button", { name: "Target tab" }).closest("li")!;
|
||||
dragOverAt(target, 16, dataTransfer);
|
||||
expect(target).toHaveAttribute("data-tab-attach-target", "true");
|
||||
expect(document.querySelector("[data-session-displaced='true']"))
|
||||
.not.toBeInTheDocument();
|
||||
expect(target).not.toHaveAttribute("data-tab-attach-target");
|
||||
expect(target).toHaveAttribute("data-session-displaced", "true");
|
||||
expect(document.querySelector("[data-pane-snap-slot]")).not.toBeInTheDocument();
|
||||
dropAt(target, 16, dataTransfer);
|
||||
|
||||
expect(onAttachPane).toHaveBeenCalledWith(
|
||||
"websocket:detached",
|
||||
expect(onAttachPane).not.toHaveBeenCalled();
|
||||
expect(onReorderSessions).toHaveBeenCalledWith([
|
||||
"websocket:target",
|
||||
);
|
||||
expect(onReorderSessions).not.toHaveBeenCalled();
|
||||
"websocket:detached",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows temporary chats separately and lets the user reopen or close them", async () => {
|
||||
@@ -611,40 +845,7 @@ describe("ChatList", () => {
|
||||
expect(within(chatsSection).queryByText("Project chat")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("positions one background highlight and resets it across hidden targets", () => {
|
||||
let revealFrame: FrameRequestCallback | null = null;
|
||||
let resizeObserverCallback: ResizeObserverCallback | null = null;
|
||||
let activeTargetVisible = true;
|
||||
class MockResizeObserver {
|
||||
constructor(callback: ResizeObserverCallback) {
|
||||
resizeObserverCallback = callback;
|
||||
}
|
||||
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
||||
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
|
||||
revealFrame = callback;
|
||||
return 1;
|
||||
});
|
||||
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
|
||||
function (this: HTMLElement) {
|
||||
if (this.hasAttribute("data-chat-list-content")) {
|
||||
return rect({ left: 0, top: 0, width: 300, height: 200 });
|
||||
}
|
||||
if (this.getAttribute("data-chat-row") === "websocket:active") {
|
||||
return activeTargetVisible
|
||||
? rect({ left: 8, top: 12, width: 284, height: 32 })
|
||||
: rect({ left: 0, top: 0, width: 0, height: 0 });
|
||||
}
|
||||
if (this.getAttribute("data-chat-row") === "websocket:inactive") {
|
||||
return rect({ left: 8, top: 48, width: 284, height: 40 });
|
||||
}
|
||||
return rect({ left: 0, top: 0, width: 0, height: 0 });
|
||||
},
|
||||
);
|
||||
it("switches row-owned tab highlights without a moving selection surface", () => {
|
||||
const props = {
|
||||
sessions: [
|
||||
session({ chatId: "active", title: "Active topic" }),
|
||||
@@ -664,45 +865,16 @@ describe("ChatList", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const highlight = screen.getByTestId("sessions-selection-highlight");
|
||||
expect(highlight).toHaveClass(
|
||||
"bg-sidebar-foreground/[0.055]",
|
||||
"transition-[transform,width,height]",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
expect(screen.queryByTestId("sessions-selection-highlight-surface"))
|
||||
.not.toBeInTheDocument();
|
||||
expect(resizeObserverCallback).not.toBeNull();
|
||||
|
||||
const activeButton = screen.getByTitle("Active topic");
|
||||
const inactiveButton = screen.getByTitle("Inactive topic");
|
||||
expect(activeButton).toHaveAttribute("aria-current", "page");
|
||||
expect(activeButton.parentElement).toHaveClass("transition-[color]");
|
||||
expect(activeButton.parentElement).not.toHaveClass("transition-colors");
|
||||
expect(activeButton.parentElement).not.toHaveClass(
|
||||
"bg-sidebar-accent",
|
||||
"shadow-[inset_0_0_0_1px_hsl(var(--sidebar-border)/0.55)]",
|
||||
expect(activeButton.closest("[data-sidebar-tab]")).toHaveClass(
|
||||
"bg-sidebar-selected",
|
||||
);
|
||||
expect(highlight).toHaveClass(
|
||||
"transition-[transform,width,height]",
|
||||
"motion-reduce:transition-none",
|
||||
expect(inactiveButton.closest("[data-sidebar-tab]")).not.toHaveClass(
|
||||
"bg-sidebar-selected",
|
||||
);
|
||||
expect(highlight).toHaveStyle(
|
||||
"width: 284px; height: 32px; transform: translate3d(8px, 12px, 0); opacity: 1; transition-property: none",
|
||||
);
|
||||
|
||||
revealFrame?.(0);
|
||||
expect(highlight.style.transitionProperty).toBe("");
|
||||
|
||||
activeTargetVisible = false;
|
||||
resizeObserverCallback?.([], {} as ResizeObserver);
|
||||
expect(highlight).toHaveStyle("opacity: 0");
|
||||
|
||||
activeTargetVisible = true;
|
||||
resizeObserverCallback?.([], {} as ResizeObserver);
|
||||
expect(highlight).toHaveStyle(
|
||||
"width: 284px; height: 32px; transform: translate3d(8px, 12px, 0); opacity: 1; transition-property: none",
|
||||
);
|
||||
revealFrame?.(0);
|
||||
expect(screen.queryByTestId("sessions-selection-highlight")).not.toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<ChatList
|
||||
@@ -713,12 +885,17 @@ describe("ChatList", () => {
|
||||
|
||||
expect(screen.getByTitle("Active topic")).not.toHaveAttribute("aria-current");
|
||||
expect(screen.getByTitle("Inactive topic")).toHaveAttribute("aria-current", "page");
|
||||
expect(highlight).toHaveStyle(
|
||||
"width: 284px; height: 40px; transform: translate3d(8px, 48px, 0)",
|
||||
expect(screen.getByTitle("Active topic").closest("[data-sidebar-tab]")).not.toHaveClass(
|
||||
"bg-sidebar-selected",
|
||||
);
|
||||
expect(screen.getByTitle("Inactive topic").closest("[data-sidebar-tab]")).toHaveClass(
|
||||
"bg-sidebar-selected",
|
||||
);
|
||||
|
||||
rerender(<ChatList {...props} activeKey={null} />);
|
||||
expect(highlight).toHaveStyle("opacity: 0");
|
||||
expect(screen.getByTitle("Inactive topic").closest("[data-sidebar-tab]")).not.toHaveClass(
|
||||
"bg-sidebar-selected",
|
||||
);
|
||||
});
|
||||
|
||||
it("can collapse a project group and keeps project rename separate from chat titles", async () => {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
paneDropSlotForRow,
|
||||
paneTabDragLayout,
|
||||
samePaneDropSlot,
|
||||
type PaneTabDragState,
|
||||
} from "@/components/pane-tab-drag";
|
||||
|
||||
function drag(overrides: Partial<PaneTabDragState> = {}): PaneTabDragState {
|
||||
return {
|
||||
origin: "pane",
|
||||
item: { paneKey: "pane-a", sourceTabKey: "tab-a" },
|
||||
height: 32,
|
||||
slot: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Pane tab drag state", () => {
|
||||
it("turns a pointer edge into one stable insertion slot", () => {
|
||||
const before = paneDropSlotForRow(
|
||||
"tab-a",
|
||||
["pane-a", "pane-b", "pane-c"],
|
||||
"pane-a",
|
||||
"pane-b",
|
||||
"before",
|
||||
);
|
||||
const after = paneDropSlotForRow(
|
||||
"tab-a",
|
||||
["pane-a", "pane-b", "pane-c"],
|
||||
"pane-a",
|
||||
"pane-b",
|
||||
"after",
|
||||
);
|
||||
|
||||
expect(before).toEqual({ tabKey: "tab-a", beforePaneKey: "pane-b" });
|
||||
expect(after).toEqual({ tabKey: "tab-a", beforePaneKey: "pane-c" });
|
||||
expect(samePaneDropSlot(after, { ...after })).toBe(true);
|
||||
});
|
||||
|
||||
it("moves the dragged slot and repels siblings inside one tab", () => {
|
||||
const layout = paneTabDragLayout(
|
||||
["pane-a", "pane-b", "pane-c"],
|
||||
"tab-a",
|
||||
drag({ slot: { tabKey: "tab-a", beforePaneKey: "pane-c" } }),
|
||||
);
|
||||
|
||||
expect(layout.slotIndex).toBe(1);
|
||||
expect(Object.fromEntries(layout.offsets)).toEqual({
|
||||
"pane-b": -34,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not expose a slot in another tab", () => {
|
||||
const layout = paneTabDragLayout(
|
||||
["pane-x", "pane-y"],
|
||||
"tab-b",
|
||||
drag({ slot: { tabKey: "tab-b", beforePaneKey: "pane-y" } }),
|
||||
);
|
||||
|
||||
expect(layout.slotIndex).toBe(-1);
|
||||
expect(Object.fromEntries(layout.offsets)).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -104,6 +104,34 @@ describe("workbench model", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("places a moved pane into an exact tab slot", () => {
|
||||
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "topic-a", "pane-a");
|
||||
state = addWorkbenchPane(state, "topic-a", "pane-b");
|
||||
state = addWorkbenchPane(state, "topic-a", "pane-c");
|
||||
|
||||
state = attachWorkbenchPane(state, "topic-a", "pane-c", "pane-a");
|
||||
expect(workbenchTab(state, "topic-a").paneKeys).toEqual([
|
||||
"topic-a",
|
||||
"pane-c",
|
||||
"pane-a",
|
||||
"pane-b",
|
||||
]);
|
||||
|
||||
state = ensureWorkbenchTab(state, "topic-b");
|
||||
state = addWorkbenchPane(state, "topic-b", "pane-d");
|
||||
state = attachWorkbenchPane(state, "topic-b", "pane-a", "pane-d");
|
||||
expect(workbenchTab(state, "topic-a").paneKeys).toEqual([
|
||||
"topic-a",
|
||||
"pane-c",
|
||||
"pane-b",
|
||||
]);
|
||||
expect(workbenchTab(state, "topic-b").paneKeys).toEqual([
|
||||
"topic-b",
|
||||
"pane-a",
|
||||
"pane-d",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not collapse a multi-pane tab into another tab", () => {
|
||||
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "topic-a", "pane-a");
|
||||
state = ensureWorkbenchTab(state, "topic-b");
|
||||
|
||||
@@ -89,6 +89,7 @@ export default {
|
||||
sidebar: {
|
||||
DEFAULT: "hsl(var(--sidebar))",
|
||||
foreground: "hsl(var(--sidebar-foreground))",
|
||||
selected: "hsl(var(--sidebar-selected))",
|
||||
accent: "hsl(var(--sidebar-accent))",
|
||||
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||
border: "hsl(var(--sidebar-border))",
|
||||
|
||||
Reference in New Issue
Block a user