From b660c8259c76451fb5d4b489e35cf7c631e723d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Mon, 20 Jul 2026 15:12:07 +0200 Subject: [PATCH 1/4] feat(ui): add right panel customization Add a small typed right panel registry so core tabs and status sections can be treated as configurable panel items. The right panel now lets users show, hide, and reorder tabs and Status subpanels, with preferences persisted in localStorage. Keep the implementation internal for now rather than adding a plugin loader; #193's full external plugin architecture is larger than this UI customization layer. Add registry unit coverage for ordering, visibility, duplicate ids, moves, and malformed persisted data. Validation: npm exec --no -- tsx --test packages/ui/src/components/instance/shell/right-panel/registry.test.ts; npm run typecheck --workspace @codenomad/ui; npm run build --workspace @codenomad/ui; git diff --check --- .../instance/shell/right-panel/RightPanel.tsx | 408 ++++++++++++++---- .../shell/right-panel/registry.test.ts | 71 +++ .../instance/shell/right-panel/registry.ts | 113 +++++ .../shell/right-panel/tabs/StatusTab.tsx | 173 +++++--- .../shell/right-panel/tabs/status-sections.ts | 46 ++ .../instance/shell/right-panel/types.ts | 4 +- .../src/components/instance/shell/storage.ts | 6 +- .../ui/src/lib/i18n/messages/en/instance.ts | 8 + .../ui/src/lib/i18n/messages/es/instance.ts | 8 + .../ui/src/lib/i18n/messages/fr/instance.ts | 8 + .../ui/src/lib/i18n/messages/he/instance.ts | 8 + .../ui/src/lib/i18n/messages/ja/instance.ts | 8 + .../ui/src/lib/i18n/messages/ru/instance.ts | 8 + .../src/lib/i18n/messages/zh-Hans/instance.ts | 8 + packages/ui/src/styles/panels/right-panel.css | 65 +++ 15 files changed, 786 insertions(+), 156 deletions(-) create mode 100644 packages/ui/src/components/instance/shell/right-panel/registry.test.ts create mode 100644 packages/ui/src/components/instance/shell/right-panel/registry.ts create mode 100644 packages/ui/src/components/instance/shell/right-panel/tabs/status-sections.ts diff --git a/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx b/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx index 5f6865fb1..817e0a2f2 100644 --- a/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx @@ -1,4 +1,5 @@ import { + For, Show, Suspense, createEffect, @@ -15,6 +16,7 @@ import IconButton from "@suid/material/IconButton" import MenuOpenIcon from "@suid/icons-material/MenuOpen" import PushPinIcon from "@suid/icons-material/PushPin" import PushPinOutlinedIcon from "@suid/icons-material/PushPinOutlined" +import { Settings2 } from "lucide-solid" import type { Instance } from "../../../../types/instance" import type { BackgroundProcess } from "../../../../../../server/src/api-types" @@ -35,7 +37,7 @@ import { requestData } from "../../../../lib/opencode-api" import { serverApi } from "../../../../lib/api-client" import { showConfirmDialog } from "../../../../stores/alerts" import { showToastNotification } from "../../../../lib/notifications" -import { writeClientLayoutValue } from "../../../../stores/client-state" +import { readClientLayoutValue, writeClientLayoutValue } from "../../../../stores/client-state" import { useGlobalPointerDrag } from "../useGlobalPointerDrag" import { useGitChanges } from "./useGitChanges" import { @@ -53,12 +55,25 @@ import { RIGHT_PANEL_GIT_CHANGES_SPLIT_WIDTH_KEY, RIGHT_PANEL_GIT_CHANGES_UNSTAGED_OPEN_NONPHONE_KEY, RIGHT_PANEL_GIT_CHANGES_UNSTAGED_OPEN_PHONE_KEY, + RIGHT_PANEL_CUSTOMIZATION_STORAGE_KEY, RIGHT_PANEL_TAB_STORAGE_KEY, readStoredBool, readStoredEnum, readStoredPanelWidth, readStoredRightPanelTab, } from "../storage" +import { + applyRightPanelItemCustomization, + collectRightPanelItems, + moveRightPanelItem, + parseRightPanelCustomization, + setRightPanelItemHidden, + type RightPanelCustomization, + type RightPanelModule, + type RightPanelSectionModule, + type RightPanelTabModule, +} from "./registry" +import { CORE_STATUS_SECTION_ITEMS } from "./tabs/status-sections" const LazyGitChangesTab = lazy(() => import("./tabs/GitChangesTab")) const LazyFilesTab = lazy(() => import("./tabs/FilesTab")) @@ -98,8 +113,13 @@ interface RightPanelProps { const RightPanel: Component = (props) => { const [rightPanelTab, setRightPanelTab] = createSignal(readStoredRightPanelTab("git-changes")) - const defaultStatusSectionIds = ["provider-usage", "yolo-mode", "plan", "background-processes", "mcp", "lsp", "plugins"] + const defaultStatusSectionIds = CORE_STATUS_SECTION_ITEMS.map((section) => section.id) const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal(defaultStatusSectionIds) + const [rightPanelCustomizationOpen, setRightPanelCustomizationOpen] = createSignal(false) + const [rightPanelCustomization, setRightPanelCustomization] = createSignal( + parseRightPanelCustomization(readClientLayoutValue(RIGHT_PANEL_CUSTOMIZATION_STORAGE_KEY)), + ) + const [draggedTabId, setDraggedTabId] = createSignal(null) const [browserPath, setBrowserPath] = createSignal(".") const [browserEntries, setBrowserEntries] = createSignal(null) @@ -648,77 +668,40 @@ const RightPanel: Component = (props) => { setRightPanelExpandedItems(values) } - const tabClass = (tab: RightPanelTab) => - `right-panel-tab ${rightPanelTab() === tab ? "right-panel-tab-active" : "right-panel-tab-inactive"}` + const updateRightPanelCustomization = (updater: (current: RightPanelCustomization) => RightPanelCustomization) => { + const next = updater(rightPanelCustomization()) + setRightPanelCustomization(next) + writeClientLayoutValue(RIGHT_PANEL_CUSTOMIZATION_STORAGE_KEY, JSON.stringify(next)) + } - return ( -
-
-
-
- - - - - - - (props.rightPinned() ? props.onUnpinRightDrawer() : props.onPinRightDrawer())} - > - {props.rightPinned() ? : } - - -
-
-
-
- - - -
+ const moveTab = (sourceId: string, targetId: string) => { + if (!sourceId || sourceId === targetId) return + const visibleIds = visibleRightPanelTabs().map((tab) => tab.id) + const sourceIndex = visibleIds.indexOf(sourceId) + const targetIndex = visibleIds.indexOf(targetId) + if (sourceIndex === -1 || targetIndex === -1) return + const nextVisibleIds = [...visibleIds] + const [moved] = nextVisibleIds.splice(sourceIndex, 1) + nextVisibleIds.splice(targetIndex, 0, moved) + const allIds = allRightPanelTabs().map((tab) => tab.id) + updateRightPanelCustomization((current) => ({ + ...current, + tabOrder: [...nextVisibleIds, ...allIds.filter((id) => !nextVisibleIds.includes(id))], + })) + } -
-
-
-
-
+ const tabClass = (tab: RightPanelTab) => + `right-panel-tab ${rightPanelTab() === tab ? "right-panel-tab-active" : "right-panel-tab-inactive"}` -
- - }> + const rightPanelModules = createMemo(() => [ + { + id: "core-right-panel", + tabs: [ + { + id: "git-changes", + labelKey: "instanceShell.rightPanel.tabs.gitChanges", + order: 10, + render: () => ( = (props) => { onResizeTouchStart={handleSplitResizeTouchStart("git-changes")} isPhoneLayout={props.isPhoneLayout} /> - - - - - }> + ), + }, + { + id: "files", + labelKey: "instanceShell.rightPanel.tabs.files", + order: 20, + render: () => ( = (props) => { onResizeTouchStart={handleSplitResizeTouchStart("files")} isPhoneLayout={props.isPhoneLayout} /> - - - - - }> + ), + }, + { + id: "status", + labelKey: "instanceShell.rightPanel.tabs.status", + order: 30, + render: () => ( = (props) => { onTerminateBackgroundProcess={props.onTerminateBackgroundProcess} expandedItems={rightPanelExpandedItems} onExpandedItemsChange={handleAccordionChange} + customization={rightPanelCustomization} + onCustomizationChange={updateRightPanelCustomization} + extraSections={extraStatusSections()} /> - + ), + }, + ], + }, + ]) + + const allRightPanelTabs = createMemo(() => collectRightPanelItems(rightPanelModules(), "tabs")) + const visibleRightPanelTabs = createMemo(() => + applyRightPanelItemCustomization( + allRightPanelTabs(), + rightPanelCustomization().tabOrder, + rightPanelCustomization().hiddenTabIds, + ), + ) + const orderedRightPanelTabs = createMemo(() => applyRightPanelItemCustomization(allRightPanelTabs(), rightPanelCustomization().tabOrder, [])) + const extraStatusSections = createMemo(() => collectRightPanelItems(rightPanelModules(), "statusSections")) + const allStatusSections = createMemo(() => [...CORE_STATUS_SECTION_ITEMS, ...extraStatusSections()]) + const orderedStatusSections = createMemo(() => applyRightPanelItemCustomization(allStatusSections(), rightPanelCustomization().statusSectionOrder, [])) + const visibleStatusSections = createMemo(() => + applyRightPanelItemCustomization( + allStatusSections(), + rightPanelCustomization().statusSectionOrder, + rightPanelCustomization().hiddenStatusSectionIds, + ), + ) + const activeRightPanelTab = createMemo(() => visibleRightPanelTabs().find((tab) => tab.id === rightPanelTab()) ?? visibleRightPanelTabs()[0]) + + createEffect(() => { + const active = activeRightPanelTab() + if (active && active.id !== rightPanelTab()) { + setRightPanelTab(active.id) + } + }) + + return ( +
+
+
+
+ + + + + + + (props.rightPinned() ? props.onUnpinRightDrawer() : props.onPinRightDrawer())} + > + {props.rightPinned() ? : } + + + setRightPanelCustomizationOpen((open) => !open)} + > + + +
+
+
+
+ + {(tab) => ( + + )} + +
+ +
+
+
+
+
+ + +
+
+
+
{props.t("instanceShell.rightPanel.customize.title")}
+
{props.t("instanceShell.rightPanel.customize.description")}
+
+ +
+ +
+
+
{props.t("instanceShell.rightPanel.customize.tabs")}
+ + {(tab) => { + const visible = () => !rightPanelCustomization().hiddenTabIds.includes(tab.id) + const disableHide = () => visible() && visibleRightPanelTabs().length <= 1 + return ( +
+ +
+ + +
+
+ ) + }} +
+
+ +
+
{props.t("instanceShell.rightPanel.customize.statusSections")}
+ + {(section) => { + const visible = () => !rightPanelCustomization().hiddenStatusSectionIds.includes(section.id) + const disableHide = () => visible() && visibleStatusSections().length <= 1 + return ( +
+ +
+ + +
+
+ ) + }} +
+
+
+
+
+ +
+ + {(tab) => }>{tab.render()}}
diff --git a/packages/ui/src/components/instance/shell/right-panel/registry.test.ts b/packages/ui/src/components/instance/shell/right-panel/registry.test.ts new file mode 100644 index 000000000..a7b39d015 --- /dev/null +++ b/packages/ui/src/components/instance/shell/right-panel/registry.test.ts @@ -0,0 +1,71 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" + +import { + applyRightPanelItemCustomization, + collectRightPanelItems, + moveRightPanelItem, + parseRightPanelCustomization, + setRightPanelItemHidden, + type RightPanelItem, + type RightPanelTabModule, +} from "./registry" + +const item = (id: string, order: number, alwaysVisible = false): RightPanelItem => ({ id, labelKey: id, order, alwaysVisible }) +const tab = (id: string, order: number): RightPanelTabModule => ({ ...item(id, order), render: () => undefined as any }) + +describe("right panel registry", () => { + it("collects and orders module items", () => { + const items = collectRightPanelItems( + [ + { id: "core", tabs: [tab("status", 40), tab("changes", 10)] }, + { id: "plugin", tabs: [tab("custom", 30)] }, + ], + "tabs", + ) + + assert.deepEqual(items.map((entry) => entry.id), ["changes", "custom", "status"]) + }) + + it("rejects duplicate item ids", () => { + assert.throws(() => collectRightPanelItems([{ id: "core", tabs: [tab("status", 10), tab("status", 20)] }], "tabs")) + }) + + it("applies visibility and user order", () => { + const visible = applyRightPanelItemCustomization( + [item("changes", 10), item("files", 20), item("status", 30)], + ["status", "changes"], + ["files"], + ) + + assert.deepEqual(visible.map((entry) => entry.id), ["status", "changes"]) + }) + + it("keeps always-visible items visible", () => { + const visible = applyRightPanelItemCustomization([item("configure", 100, true)], [], ["configure"]) + + assert.deepEqual(visible.map((entry) => entry.id), ["configure"]) + }) + + it("moves items in normalized order", () => { + assert.deepEqual(moveRightPanelItem(["changes", "status", "files"], ["changes", "files", "status"], "status", -1), [ + "status", + "changes", + "files", + ]) + }) + + it("parses invalid customization safely", () => { + assert.deepEqual(parseRightPanelCustomization("{"), { + tabOrder: [], + hiddenTabIds: [], + statusSectionOrder: [], + hiddenStatusSectionIds: [], + }) + }) + + it("toggles hidden ids", () => { + assert.deepEqual(setRightPanelItemHidden(["files"], "status", true), ["files", "status"]) + assert.deepEqual(setRightPanelItemHidden(["files"], "files", false), []) + }) +}) diff --git a/packages/ui/src/components/instance/shell/right-panel/registry.ts b/packages/ui/src/components/instance/shell/right-panel/registry.ts new file mode 100644 index 000000000..c24077eac --- /dev/null +++ b/packages/ui/src/components/instance/shell/right-panel/registry.ts @@ -0,0 +1,113 @@ +import type { JSX } from "solid-js" + +export interface RightPanelItem { + id: string + labelKey: string + order: number + alwaysVisible?: boolean +} + +export interface RightPanelTabModule extends RightPanelItem { + render: () => JSX.Element +} + +export interface RightPanelSectionModule extends RightPanelItem { + tooltipKey: string + defaultExpanded?: boolean + render: () => JSX.Element +} + +export interface RightPanelModule { + id: string + tabs?: readonly RightPanelTabModule[] + statusSections?: readonly RightPanelSectionModule[] +} + +export interface RightPanelCustomization { + tabOrder: string[] + hiddenTabIds: string[] + statusSectionOrder: string[] + hiddenStatusSectionIds: string[] +} + +export const DEFAULT_RIGHT_PANEL_CUSTOMIZATION: RightPanelCustomization = { + tabOrder: [], + hiddenTabIds: [], + statusSectionOrder: [], + hiddenStatusSectionIds: [], +} + +export function parseRightPanelCustomization(value: string | null): RightPanelCustomization { + if (!value) return { ...DEFAULT_RIGHT_PANEL_CUSTOMIZATION } + try { + const parsed = JSON.parse(value) as Partial + return { + tabOrder: readStringArray(parsed.tabOrder), + hiddenTabIds: readStringArray(parsed.hiddenTabIds), + statusSectionOrder: readStringArray(parsed.statusSectionOrder), + hiddenStatusSectionIds: readStringArray(parsed.hiddenStatusSectionIds), + } + } catch { + return { ...DEFAULT_RIGHT_PANEL_CUSTOMIZATION } + } +} + +export function collectRightPanelItems(modules: readonly RightPanelModule[], key: "tabs" | "statusSections"): T[] { + const items = modules.flatMap((module) => [...((module[key] ?? []) as unknown as readonly T[])]) + const seen = new Set() + for (const item of items) { + if (!item.id) throw new Error(`Right panel ${key} must define an id`) + if (seen.has(item.id)) throw new Error(`Duplicate right panel ${key} id: ${item.id}`) + seen.add(item.id) + } + return sortRightPanelItems(items, []) +} + +export function applyRightPanelItemCustomization( + items: readonly T[], + orderedIds: readonly string[], + hiddenIds: readonly string[], +): T[] { + const hidden = new Set(hiddenIds) + const visible = items.filter((item) => item.alwaysVisible || !hidden.has(item.id)) + return sortRightPanelItems(visible.length > 0 ? visible : items.slice(0, 1), orderedIds) +} + +export function moveRightPanelItem(orderedIds: readonly string[], allIds: readonly string[], id: string, direction: -1 | 1): string[] { + const order = normalizeOrder(orderedIds, allIds) + const index = order.indexOf(id) + const nextIndex = index + direction + if (index === -1 || nextIndex < 0 || nextIndex >= order.length) return order + const next = [...order] + ;[next[index], next[nextIndex]] = [next[nextIndex], next[index]] + return next +} + +export function setRightPanelItemHidden(hiddenIds: readonly string[], id: string, hidden: boolean): string[] { + const next = new Set(hiddenIds) + if (hidden) next.add(id) + else next.delete(id) + return [...next] +} + +function sortRightPanelItems(items: readonly T[], orderedIds: readonly string[]): T[] { + const rank = new Map(orderedIds.map((id, index) => [id, index])) + return [...items].sort((left, right) => { + const leftRank = rank.get(left.id) + const rightRank = rank.get(right.id) + if (leftRank !== undefined || rightRank !== undefined) { + return (leftRank ?? Number.MAX_SAFE_INTEGER) - (rightRank ?? Number.MAX_SAFE_INTEGER) + } + return left.order - right.order || left.id.localeCompare(right.id) + }) +} + +function normalizeOrder(orderedIds: readonly string[], allIds: readonly string[]): string[] { + const known = new Set(allIds) + const ordered = orderedIds.filter((id, index) => known.has(id) && orderedIds.indexOf(id) === index) + return [...ordered, ...allIds.filter((id) => !ordered.includes(id))] +} + +function readStringArray(value: unknown): string[] { + return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [] +} diff --git a/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx b/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx index 8bdc246a7..c95db83ef 100644 --- a/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx @@ -1,10 +1,10 @@ -import { For, Show, type Accessor, type Component } from "solid-js" +import { For, Show, createMemo, createSignal, type Accessor, type Component } from "solid-js" import type { ToolState } from "@opencode-ai/sdk/v2" import { Accordion } from "@kobalte/core" import { Tooltip } from "@kobalte/core/tooltip" import Switch from "@suid/material/Switch" -import { BellRing, ChevronDown, Info, TerminalSquare, Trash2, XOctagon } from "lucide-solid" +import { BellRing, ChevronDown, GripVertical, Info, TerminalSquare, Trash2, XOctagon } from "lucide-solid" import type { Instance } from "../../../../../types/instance" import type { BackgroundProcess } from "../../../../../../../server/src/api-types" @@ -16,6 +16,7 @@ import { TodoListView } from "../../../../tool-call/renderers/todo" import InstanceServiceStatus from "../../../../instance-service-status" import { togglePermissionAutoAcceptForSession } from "../../../../../stores/instances" import { isPermissionAutoAcceptEnabled } from "../../../../../stores/permission-auto-accept" +import { applyRightPanelItemCustomization, type RightPanelCustomization, type RightPanelSectionModule } from "../registry" interface StatusTabProps { t: (key: string, vars?: Record) => string @@ -35,11 +36,14 @@ interface StatusTabProps { expandedItems: Accessor onExpandedItemsChange: (values: string[]) => void - + customization: Accessor + onCustomizationChange: (updater: (current: RightPanelCustomization) => RightPanelCustomization) => void + extraSections?: readonly RightPanelSectionModule[] } const StatusTab: Component = (props) => { const isSectionExpanded = (id: string) => props.expandedItems().includes(id) + const [draggedSectionId, setDraggedSectionId] = createSignal(null) const renderYoloModeSection = () => { const session = props.activeSession() @@ -182,71 +186,78 @@ const StatusTab: Component = (props) => { ) } - const statusSections = [ - { - id: "yolo-mode", - labelKey: "instanceShell.rightPanel.sections.yoloMode", - tooltipKey: "instanceShell.rightPanel.sections.yoloMode.tooltip", - render: renderYoloModeSection, - }, - { - id: "provider-usage", - labelKey: "providerUsage.title", - tooltipKey: "providerUsage.tooltip", - render: renderProviderUsage, - }, - { - id: "plan", - labelKey: "instanceShell.rightPanel.sections.plan", - tooltipKey: "instanceShell.rightPanel.sections.plan.tooltip", - render: renderPlanSectionContent, - }, - { - id: "background-processes", - labelKey: "instanceShell.rightPanel.sections.backgroundProcesses", - tooltipKey: "instanceShell.rightPanel.sections.backgroundProcesses.tooltip", - render: renderBackgroundProcesses, - }, - { - id: "mcp", - labelKey: "instanceShell.rightPanel.sections.mcp", - tooltipKey: "instanceShell.rightPanel.sections.mcp.tooltip", - render: () => ( - - ), - }, - { - id: "lsp", - labelKey: "instanceShell.rightPanel.sections.lsp", - tooltipKey: "instanceShell.rightPanel.sections.lsp.tooltip", - render: () => ( - - ), - }, - { - id: "plugins", - labelKey: "instanceShell.rightPanel.sections.plugins", - tooltipKey: "instanceShell.rightPanel.sections.plugins.tooltip", - render: () => ( - - ), - }, - ] + const statusSections = createMemo(() => { + const sections: RightPanelSectionModule[] = [ + { + id: "provider-usage", + labelKey: "providerUsage.title", + tooltipKey: "providerUsage.tooltip", + order: 10, + render: renderProviderUsage, + }, + { + id: "yolo-mode", + labelKey: "instanceShell.rightPanel.sections.yoloMode", + tooltipKey: "instanceShell.rightPanel.sections.yoloMode.tooltip", + order: 20, + render: renderYoloModeSection, + }, + { + id: "plan", + labelKey: "instanceShell.rightPanel.sections.plan", + tooltipKey: "instanceShell.rightPanel.sections.plan.tooltip", + order: 30, + render: renderPlanSectionContent, + }, + { + id: "background-processes", + labelKey: "instanceShell.rightPanel.sections.backgroundProcesses", + tooltipKey: "instanceShell.rightPanel.sections.backgroundProcesses.tooltip", + order: 40, + render: renderBackgroundProcesses, + }, + { + id: "mcp", + labelKey: "instanceShell.rightPanel.sections.mcp", + tooltipKey: "instanceShell.rightPanel.sections.mcp.tooltip", + order: 50, + render: () => , + }, + { + id: "lsp", + labelKey: "instanceShell.rightPanel.sections.lsp", + tooltipKey: "instanceShell.rightPanel.sections.lsp.tooltip", + order: 60, + render: () => , + }, + { + id: "plugins", + labelKey: "instanceShell.rightPanel.sections.plugins", + tooltipKey: "instanceShell.rightPanel.sections.plugins.tooltip", + order: 70, + render: () => ( + + ), + }, + ] + return applyRightPanelItemCustomization( + [...sections, ...(props.extraSections ?? [])], + props.customization().statusSectionOrder, + props.customization().hiddenStatusSectionIds, + ) + }) + + const moveSection = (sourceId: string, targetId: string) => { + if (!sourceId || sourceId === targetId) return + const ids = statusSections().map((section) => section.id) + const sourceIndex = ids.indexOf(sourceId) + const targetIndex = ids.indexOf(targetId) + if (sourceIndex === -1 || targetIndex === -1) return + const next = [...ids] + const [moved] = next.splice(sourceIndex, 1) + next.splice(targetIndex, 0, moved) + props.onCustomizationChange((current) => ({ ...current, statusSectionOrder: next })) + } return (
@@ -263,12 +274,34 @@ const StatusTab: Component = (props) => { value={props.expandedItems()} onChange={props.onExpandedItemsChange} > - + {(section) => ( - + { + setDraggedSectionId(section.id) + event.dataTransfer?.setData("text/plain", section.id) + if (event.dataTransfer) event.dataTransfer.effectAllowed = "move" + }} + onDragOver={(event) => { + if (!draggedSectionId() || draggedSectionId() === section.id) return + event.preventDefault() + if (event.dataTransfer) event.dataTransfer.dropEffect = "move" + }} + onDrop={(event) => { + event.preventDefault() + const sourceId = event.dataTransfer?.getData("text/plain") || draggedSectionId() + if (sourceId) moveSection(sourceId, section.id) + setDraggedSectionId(null) + }} + onDragEnd={() => setDraggedSectionId(null)} + > + Date: Mon, 20 Jul 2026 15:50:00 +0200 Subject: [PATCH 2/4] fix(ui): polish right panel customization Tighten the right panel customization UI after review by adding contextual move labels for assistive tech, making the drawer layout fit the narrow 200px panel, and removing newly introduced rounded styling. Complete the customization translation keys for German and Nepali and add the contextual move labels to all supported locales. Restore the default Status section order so Yolo mode remains before provider usage while keeping user reordering intact. Validated with the focused registry test, UI typecheck, diff whitespace check, UI build, and a final gatekeeper pass. --- .../instance/shell/right-panel/RightPanel.tsx | 24 +++++++---- .../shell/right-panel/tabs/StatusTab.tsx | 16 ++++---- .../shell/right-panel/tabs/status-sections.ts | 12 +++--- .../ui/src/lib/i18n/messages/de/instance.ts | 12 ++++++ .../ui/src/lib/i18n/messages/en/instance.ts | 4 ++ .../ui/src/lib/i18n/messages/es/instance.ts | 4 ++ .../ui/src/lib/i18n/messages/fr/instance.ts | 4 ++ .../ui/src/lib/i18n/messages/he/instance.ts | 4 ++ .../ui/src/lib/i18n/messages/ja/instance.ts | 4 ++ .../ui/src/lib/i18n/messages/ne/instance.ts | 12 ++++++ .../ui/src/lib/i18n/messages/ru/instance.ts | 4 ++ .../src/lib/i18n/messages/zh-Hans/instance.ts | 4 ++ packages/ui/src/styles/panels/right-panel.css | 40 +++++++++++++++---- 13 files changed, 115 insertions(+), 29 deletions(-) diff --git a/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx b/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx index 817e0a2f2..205ea2c16 100644 --- a/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx @@ -931,7 +931,7 @@ const RightPanel: Component = (props) => {
- -
) }} @@ -1025,52 +996,13 @@ const RightPanel: Component = (props) => { /> {label()} -
- - -
) }}
+
{props.t("instanceShell.rightPanel.customize.dragToReorder")}
diff --git a/packages/ui/src/lib/i18n/messages/de/instance.ts b/packages/ui/src/lib/i18n/messages/de/instance.ts index 01e58ad3a..5b5e9018c 100644 --- a/packages/ui/src/lib/i18n/messages/de/instance.ts +++ b/packages/ui/src/lib/i18n/messages/de/instance.ts @@ -95,10 +95,11 @@ export const instanceMessages = { "instanceShell.rightPanel.tabs.ariaLabel": "Tabs im rechten Panel", "instanceShell.rightPanel.customize.toggle": "Rechtes Panel anpassen", "instanceShell.rightPanel.customize.title": "Rechtes Panel anpassen", - "instanceShell.rightPanel.customize.description": "Tabs und Statusabschnitte anzeigen, ausblenden und neu anordnen.", + "instanceShell.rightPanel.customize.description": "Tabs und Statusabschnitte anzeigen oder ausblenden.", "instanceShell.rightPanel.customize.reset": "Zurücksetzen", "instanceShell.rightPanel.customize.tabs": "Tabs", "instanceShell.rightPanel.customize.statusSections": "Statusabschnitte", + "instanceShell.rightPanel.customize.dragToReorder": "Tabs oder Statusabschnitte ziehen, um sie neu anzuordnen.", "instanceShell.rightPanel.customize.moveUp": "Nach oben", "instanceShell.rightPanel.customize.moveDown": "Nach unten", "instanceShell.rightPanel.customize.moveTabUp": "Tab {label} nach oben verschieben", diff --git a/packages/ui/src/lib/i18n/messages/en/instance.ts b/packages/ui/src/lib/i18n/messages/en/instance.ts index 59597729f..f3371780b 100644 --- a/packages/ui/src/lib/i18n/messages/en/instance.ts +++ b/packages/ui/src/lib/i18n/messages/en/instance.ts @@ -95,10 +95,11 @@ export const instanceMessages = { "instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs", "instanceShell.rightPanel.customize.toggle": "Customize right panel", "instanceShell.rightPanel.customize.title": "Customize right panel", - "instanceShell.rightPanel.customize.description": "Show, hide, and reorder tabs and status sections.", + "instanceShell.rightPanel.customize.description": "Show or hide tabs and status sections.", "instanceShell.rightPanel.customize.reset": "Reset", "instanceShell.rightPanel.customize.tabs": "Tabs", "instanceShell.rightPanel.customize.statusSections": "Status sections", + "instanceShell.rightPanel.customize.dragToReorder": "Drag tabs or status sections to reorder.", "instanceShell.rightPanel.customize.moveUp": "Up", "instanceShell.rightPanel.customize.moveDown": "Down", "instanceShell.rightPanel.customize.moveTabUp": "Move {label} tab up", diff --git a/packages/ui/src/lib/i18n/messages/es/instance.ts b/packages/ui/src/lib/i18n/messages/es/instance.ts index c3dfda6c5..a8237d414 100644 --- a/packages/ui/src/lib/i18n/messages/es/instance.ts +++ b/packages/ui/src/lib/i18n/messages/es/instance.ts @@ -95,10 +95,11 @@ export const instanceMessages = { "instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho", "instanceShell.rightPanel.customize.toggle": "Personalizar panel derecho", "instanceShell.rightPanel.customize.title": "Personalizar panel derecho", - "instanceShell.rightPanel.customize.description": "Muestra, oculta y reordena pestañas y secciones de estado.", + "instanceShell.rightPanel.customize.description": "Muestra u oculta pestañas y secciones de estado.", "instanceShell.rightPanel.customize.reset": "Restablecer", "instanceShell.rightPanel.customize.tabs": "Pestañas", "instanceShell.rightPanel.customize.statusSections": "Secciones de estado", + "instanceShell.rightPanel.customize.dragToReorder": "Arrastra pestañas o secciones de estado para reordenarlas.", "instanceShell.rightPanel.customize.moveUp": "Subir", "instanceShell.rightPanel.customize.moveDown": "Bajar", "instanceShell.rightPanel.customize.moveTabUp": "Subir pestaña {label}", diff --git a/packages/ui/src/lib/i18n/messages/fr/instance.ts b/packages/ui/src/lib/i18n/messages/fr/instance.ts index c63326c4e..87b84b634 100644 --- a/packages/ui/src/lib/i18n/messages/fr/instance.ts +++ b/packages/ui/src/lib/i18n/messages/fr/instance.ts @@ -95,10 +95,11 @@ export const instanceMessages = { "instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit", "instanceShell.rightPanel.customize.toggle": "Personnaliser le panneau droit", "instanceShell.rightPanel.customize.title": "Personnaliser le panneau droit", - "instanceShell.rightPanel.customize.description": "Afficher, masquer et réordonner les onglets et sections de statut.", + "instanceShell.rightPanel.customize.description": "Afficher ou masquer les onglets et sections de statut.", "instanceShell.rightPanel.customize.reset": "Réinitialiser", "instanceShell.rightPanel.customize.tabs": "Onglets", "instanceShell.rightPanel.customize.statusSections": "Sections de statut", + "instanceShell.rightPanel.customize.dragToReorder": "Faites glisser les onglets ou sections de statut pour les réordonner.", "instanceShell.rightPanel.customize.moveUp": "Monter", "instanceShell.rightPanel.customize.moveDown": "Descendre", "instanceShell.rightPanel.customize.moveTabUp": "Monter l'onglet {label}", diff --git a/packages/ui/src/lib/i18n/messages/he/instance.ts b/packages/ui/src/lib/i18n/messages/he/instance.ts index e4ea417d2..4d5a4af43 100644 --- a/packages/ui/src/lib/i18n/messages/he/instance.ts +++ b/packages/ui/src/lib/i18n/messages/he/instance.ts @@ -95,10 +95,11 @@ export const instanceMessages = { "instanceShell.rightPanel.tabs.ariaLabel": "לשוניות לוח ימני", "instanceShell.rightPanel.customize.toggle": "התאמת הלוח הימני", "instanceShell.rightPanel.customize.title": "התאמת הלוח הימני", - "instanceShell.rightPanel.customize.description": "הצגה, הסתרה וסידור מחדש של כרטיסיות ומקטעי סטטוס.", + "instanceShell.rightPanel.customize.description": "הצגה או הסתרה של כרטיסיות ומקטעי סטטוס.", "instanceShell.rightPanel.customize.reset": "איפוס", "instanceShell.rightPanel.customize.tabs": "כרטיסיות", "instanceShell.rightPanel.customize.statusSections": "מקטעי סטטוס", + "instanceShell.rightPanel.customize.dragToReorder": "גררו כרטיסיות או מקטעי סטטוס כדי לסדר מחדש.", "instanceShell.rightPanel.customize.moveUp": "למעלה", "instanceShell.rightPanel.customize.moveDown": "למטה", "instanceShell.rightPanel.customize.moveTabUp": "העברת הכרטיסייה {label} למעלה", diff --git a/packages/ui/src/lib/i18n/messages/ja/instance.ts b/packages/ui/src/lib/i18n/messages/ja/instance.ts index 2a90e7908..b4eb22255 100644 --- a/packages/ui/src/lib/i18n/messages/ja/instance.ts +++ b/packages/ui/src/lib/i18n/messages/ja/instance.ts @@ -95,10 +95,11 @@ export const instanceMessages = { "instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ", "instanceShell.rightPanel.customize.toggle": "右パネルをカスタマイズ", "instanceShell.rightPanel.customize.title": "右パネルをカスタマイズ", - "instanceShell.rightPanel.customize.description": "タブとステータスセクションの表示、非表示、並べ替えを行います。", + "instanceShell.rightPanel.customize.description": "タブとステータスセクションの表示/非表示を切り替えます。", "instanceShell.rightPanel.customize.reset": "リセット", "instanceShell.rightPanel.customize.tabs": "タブ", "instanceShell.rightPanel.customize.statusSections": "ステータスセクション", + "instanceShell.rightPanel.customize.dragToReorder": "タブまたはステータスセクションをドラッグして並べ替えます。", "instanceShell.rightPanel.customize.moveUp": "上へ", "instanceShell.rightPanel.customize.moveDown": "下へ", "instanceShell.rightPanel.customize.moveTabUp": "{label} タブを上へ移動", diff --git a/packages/ui/src/lib/i18n/messages/ne/instance.ts b/packages/ui/src/lib/i18n/messages/ne/instance.ts index 943d0328a..7f5e938a4 100644 --- a/packages/ui/src/lib/i18n/messages/ne/instance.ts +++ b/packages/ui/src/lib/i18n/messages/ne/instance.ts @@ -95,10 +95,11 @@ export const instanceMessages = { "instanceShell.rightPanel.tabs.ariaLabel": "दायाँ प्यानल ट्याबहरू", "instanceShell.rightPanel.customize.toggle": "दायाँ प्यानल अनुकूलन गर्नुहोस्", "instanceShell.rightPanel.customize.title": "दायाँ प्यानल अनुकूलन गर्नुहोस्", - "instanceShell.rightPanel.customize.description": "ट्याबहरू र स्थिति खण्डहरू देखाउनुहोस्, लुकाउनुहोस् र पुनःक्रमबद्ध गर्नुहोस्।", + "instanceShell.rightPanel.customize.description": "ट्याबहरू र स्थिति खण्डहरू देखाउनुहोस् वा लुकाउनुहोस्।", "instanceShell.rightPanel.customize.reset": "रिसेट गर्नुहोस्", "instanceShell.rightPanel.customize.tabs": "ट्याबहरू", "instanceShell.rightPanel.customize.statusSections": "स्थिति खण्डहरू", + "instanceShell.rightPanel.customize.dragToReorder": "पुनःक्रमबद्ध गर्न ट्याब वा स्थिति खण्डहरू तान्नुहोस्।", "instanceShell.rightPanel.customize.moveUp": "माथि", "instanceShell.rightPanel.customize.moveDown": "तल", "instanceShell.rightPanel.customize.moveTabUp": "{label} ट्याब माथि सार्नुहोस्", diff --git a/packages/ui/src/lib/i18n/messages/ru/instance.ts b/packages/ui/src/lib/i18n/messages/ru/instance.ts index 4a937cbaa..44386c157 100644 --- a/packages/ui/src/lib/i18n/messages/ru/instance.ts +++ b/packages/ui/src/lib/i18n/messages/ru/instance.ts @@ -95,10 +95,11 @@ export const instanceMessages = { "instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели", "instanceShell.rightPanel.customize.toggle": "Настроить правую панель", "instanceShell.rightPanel.customize.title": "Настроить правую панель", - "instanceShell.rightPanel.customize.description": "Показывайте, скрывайте и меняйте порядок вкладок и секций статуса.", + "instanceShell.rightPanel.customize.description": "Показывайте или скрывайте вкладки и секции статуса.", "instanceShell.rightPanel.customize.reset": "Сбросить", "instanceShell.rightPanel.customize.tabs": "Вкладки", "instanceShell.rightPanel.customize.statusSections": "Секции статуса", + "instanceShell.rightPanel.customize.dragToReorder": "Перетаскивайте вкладки или секции статуса, чтобы изменить порядок.", "instanceShell.rightPanel.customize.moveUp": "Вверх", "instanceShell.rightPanel.customize.moveDown": "Вниз", "instanceShell.rightPanel.customize.moveTabUp": "Переместить вкладку {label} вверх", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/instance.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/instance.ts index 418310e65..f1bb74eb6 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/instance.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/instance.ts @@ -95,10 +95,11 @@ export const instanceMessages = { "instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页", "instanceShell.rightPanel.customize.toggle": "自定义右侧面板", "instanceShell.rightPanel.customize.title": "自定义右侧面板", - "instanceShell.rightPanel.customize.description": "显示、隐藏并重新排序标签页和状态分区。", + "instanceShell.rightPanel.customize.description": "显示或隐藏标签页和状态分区。", "instanceShell.rightPanel.customize.reset": "重置", "instanceShell.rightPanel.customize.tabs": "标签页", "instanceShell.rightPanel.customize.statusSections": "状态分区", + "instanceShell.rightPanel.customize.dragToReorder": "拖动标签页或状态分区以重新排序。", "instanceShell.rightPanel.customize.moveUp": "上移", "instanceShell.rightPanel.customize.moveDown": "下移", "instanceShell.rightPanel.customize.moveTabUp": "上移 {label} 标签页", diff --git a/packages/ui/src/styles/panels/right-panel.css b/packages/ui/src/styles/panels/right-panel.css index 086d22fdc..ef9889c9f 100644 --- a/packages/ui/src/styles/panels/right-panel.css +++ b/packages/ui/src/styles/panels/right-panel.css @@ -38,6 +38,7 @@ .right-panel-tab { @apply inline-flex items-center gap-2 px-4 py-2 text-xs font-medium transition-all duration-150 relative; font-family: var(--font-family-sans); + cursor: grab; outline: none; border: 1px solid transparent; border-bottom: none; @@ -768,9 +769,15 @@ min-height: 0; } -.right-panel-customization-panel { - border-bottom: 1px solid var(--border-base); +.right-panel-customization-popover { + position: absolute; + top: 2.75rem; + inset-inline-start: 0.5rem; + z-index: 20; + width: min(18rem, calc(100% - 1rem)); + border: 1px solid var(--border-base); background-color: var(--surface-secondary); + box-shadow: var(--popover-shadow); padding: 0.75rem; } @@ -786,7 +793,7 @@ .right-panel-customization-grid { display: grid; grid-template-columns: 1fr; - gap: 0.75rem; + gap: 0.5rem; } .right-panel-customization-group { @@ -805,8 +812,7 @@ .right-panel-customization-row { display: flex; - flex-direction: column; - align-items: stretch; + align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.25rem 0; @@ -821,18 +827,6 @@ font-size: 0.75rem; } -.right-panel-customization-actions { - display: flex; - align-items: center; - gap: 0.25rem; - width: 100%; -} - -.right-panel-customization-actions > button { - flex: 1 1 0; - min-width: 0; -} - .right-panel-customization-button { display: inline-flex; align-items: center; @@ -857,6 +851,13 @@ box-shadow: 0 0 0 2px var(--focus-ring-offset), 0 0 0 4px var(--focus-ring-color); } +.right-panel-customization-hint { + margin-top: 0.5rem; + color: var(--text-tertiary); + font-size: 0.6875rem; + line-height: 1rem; +} + /* Section info tooltip */ .section-info-trigger { @apply inline-flex items-center justify-center p-0.5 rounded transition-all duration-150; From 6bcc52567db2a0cc6e2202c6a7bcb0563eae7ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sat, 25 Jul 2026 13:04:25 +0200 Subject: [PATCH 4/4] fix(ui): make right panel drag reorder work Replace the native HTML drag handlers on right-panel tabs and Status sections with the same solid-dnd sortable primitives used by the main instance tabs. This keeps direct drag-to-reorder interaction reliable across the panel customization UI while preserving the existing persisted order model. Validation: npm exec --no -- tsx --test packages/ui/src/components/instance/shell/right-panel/registry.test.ts; npm run typecheck --workspace @codenomad/ui; git diff --check. --- .../instance/shell/right-panel/RightPanel.tsx | 90 +++++++++------ .../shell/right-panel/tabs/StatusTab.tsx | 106 ++++++++++-------- packages/ui/src/styles/panels/right-panel.css | 12 ++ 3 files changed, 124 insertions(+), 84 deletions(-) diff --git a/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx b/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx index e036abcb8..64f195471 100644 --- a/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx @@ -12,6 +12,14 @@ import { } from "solid-js" import type { ToolState } from "@opencode-ai/sdk/v2" import type { FileContent, FileNode } from "@opencode-ai/sdk/v2/client" +import { + DragDropProvider, + DragDropSensors, + SortableProvider, + closestCenter, + createSortable, + type DragEvent as SolidDndDragEvent, +} from "@thisbeyond/solid-dnd" import IconButton from "@suid/material/IconButton" import MenuOpenIcon from "@suid/icons-material/MenuOpen" import PushPinIcon from "@suid/icons-material/PushPin" @@ -82,6 +90,32 @@ function RightPanelTabFallback() { return
} +interface SortableRightPanelTabProps { + tab: RightPanelTabModule + active: boolean + label: string + dragTitle: string + onSelect: () => void +} + +const SortableRightPanelTab: Component = (props) => { + const sortable = createSortable(props.tab.id) + return ( +
+ +
+ ) +} + interface RightPanelProps { t: (key: string, vars?: Record) => string @@ -118,7 +152,6 @@ const RightPanel: Component = (props) => { const [rightPanelCustomization, setRightPanelCustomization] = createSignal( parseRightPanelCustomization(readClientLayoutValue(RIGHT_PANEL_CUSTOMIZATION_STORAGE_KEY)), ) - const [draggedTabId, setDraggedTabId] = createSignal(null) const [browserPath, setBrowserPath] = createSignal(".") const [browserEntries, setBrowserEntries] = createSignal(null) @@ -689,8 +722,10 @@ const RightPanel: Component = (props) => { })) } - const tabClass = (tab: RightPanelTab) => - `right-panel-tab ${rightPanelTab() === tab ? "right-panel-tab-active" : "right-panel-tab-inactive"}` + const handleTabDragEnd = ({ draggable, droppable }: SolidDndDragEvent) => { + if (!droppable) return + moveTab(String(draggable.id), String(droppable.id)) + } const rightPanelModules = createMemo(() => [ { @@ -883,38 +918,23 @@ const RightPanel: Component = (props) => {
- - {(tab) => ( - - )} - + + + tab.id)}> + + {(tab) => ( + setRightPanelTab(tab.id)} + /> + )} + + + +
diff --git a/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx b/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx index 41a98d97e..84baefd15 100644 --- a/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx @@ -1,5 +1,13 @@ -import { For, Show, createMemo, createSignal, type Accessor, type Component } from "solid-js" +import { For, Show, createMemo, type Accessor, type Component } from "solid-js" import type { ToolState } from "@opencode-ai/sdk/v2" +import { + DragDropProvider, + DragDropSensors, + SortableProvider, + closestCenter, + createSortable, + type DragEvent as SolidDndDragEvent, +} from "@thisbeyond/solid-dnd" import { Accordion } from "@kobalte/core" import { Tooltip } from "@kobalte/core/tooltip" import Switch from "@suid/material/Switch" @@ -41,9 +49,42 @@ interface StatusTabProps { extraSections?: readonly RightPanelSectionModule[] } +interface SortableStatusSectionProps { + section: RightPanelSectionModule + expanded: boolean + t: (key: string, vars?: Record) => string +} + +const SortableStatusSection: Component = (props) => { + const sortable = createSortable(props.section.id) + return ( +
+ + + + + + + + + + + + + + + {props.section.render()} + +
+ ) +} + const StatusTab: Component = (props) => { const isSectionExpanded = (id: string) => props.expandedItems().includes(id) - const [draggedSectionId, setDraggedSectionId] = createSignal(null) const renderYoloModeSection = () => { const session = props.activeSession() @@ -259,6 +300,11 @@ const StatusTab: Component = (props) => { props.onCustomizationChange((current) => ({ ...current, statusSectionOrder: next })) } + const handleSectionDragEnd = ({ draggable, droppable }: SolidDndDragEvent) => { + if (!droppable) return + moveSection(String(draggable.id), String(droppable.id)) + } + return (
@@ -274,53 +320,15 @@ const StatusTab: Component = (props) => { value={props.expandedItems()} onChange={props.onExpandedItemsChange} > - - {(section) => ( - { - setDraggedSectionId(section.id) - event.dataTransfer?.setData("text/plain", section.id) - if (event.dataTransfer) event.dataTransfer.effectAllowed = "move" - }} - onDragOver={(event) => { - if (!draggedSectionId() || draggedSectionId() === section.id) return - event.preventDefault() - if (event.dataTransfer) event.dataTransfer.dropEffect = "move" - }} - onDrop={(event) => { - event.preventDefault() - const sourceId = event.dataTransfer?.getData("text/plain") || draggedSectionId() - if (sourceId) moveSection(sourceId, section.id) - setDraggedSectionId(null) - }} - onDragEnd={() => setDraggedSectionId(null)} - > - - - - - - - - - - - - - - {section.render()} - - )} - + + + section.id)}> + + {(section) => } + + + +
) diff --git a/packages/ui/src/styles/panels/right-panel.css b/packages/ui/src/styles/panels/right-panel.css index ef9889c9f..1d0bb460c 100644 --- a/packages/ui/src/styles/panels/right-panel.css +++ b/packages/ui/src/styles/panels/right-panel.css @@ -47,6 +47,14 @@ z-index: 1; } +.right-panel-tab-draggable { + display: inline-flex; +} + +.right-panel-tab-draggable-active { + opacity: 0.75; +} + .right-panel-tab:focus-visible { @apply ring-2 ring-offset-1; ring-color: var(--accent-primary); @@ -769,6 +777,10 @@ min-height: 0; } +.right-panel-section-draggable-active { + opacity: 0.75; +} + .right-panel-customization-popover { position: absolute; top: 2.75rem;