diff --git a/packages/app/e2e/settings/settings-close-on-nav.spec.ts b/packages/app/e2e/settings/settings-close-on-nav.spec.ts new file mode 100644 index 000000000..a139c3a73 --- /dev/null +++ b/packages/app/e2e/settings/settings-close-on-nav.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from "../fixtures" +import { openSettings, openSidebar, withSession } from "../actions" +import { pawworkSidebarSelector } from "../selectors" + +test("clicking a sidebar session closes the settings overlay and navigates", async ({ + page, + sdk, + gotoSession, +}) => { + const stamp = Date.now() + await withSession(sdk, `settings-nav a ${stamp}`, async (a) => { + await withSession(sdk, `settings-nav b ${stamp}`, async (b) => { + await gotoSession(a.id) + await openSidebar(page) + + const settings = await openSettings(page) + await expect(settings).toBeVisible() + + const sidebar = page.locator(pawworkSidebarSelector).first() + await sidebar.locator(`[data-session-id="${b.id}"]`).first().click() + + await expect(settings).toBeHidden() + await expect.poll(() => page.url()).toContain(b.id) + }) + }) +}) diff --git a/packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts b/packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts index ee22c9baf..a16755c93 100644 --- a/packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts +++ b/packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts @@ -44,13 +44,8 @@ test("pawwork sidebar merges pin into the status slot with a stable title baseli await pinnedRow.hover() expect(await leftOf(pinnedTitle)).toBe(baseline) - // Pin button should occupy the row's leading slot, not a separate column. - const pinButton = pinnedRow.locator('[data-action="pawwork-session-pin"][data-pinned="true"]').first() - await expect(pinButton).toBeVisible() - const pinRect = await pinButton.evaluate((el) => el.getBoundingClientRect()) - const rowRect = await pinnedRow.evaluate((el) => el.getBoundingClientRect()) - expect(Math.round(pinRect.left - rowRect.left)).toBe(8) - expect(Math.round(pinRect.width)).toBe(24) + // Per L35 the pin glyph is removed from the row entirely; pin status is + // signaled by the row's presence in the Pinned section (asserted above). }) }) diff --git a/packages/app/e2e/sidebar/sidebar-project-collapse.spec.ts b/packages/app/e2e/sidebar/sidebar-project-collapse.spec.ts new file mode 100644 index 000000000..f38a06414 --- /dev/null +++ b/packages/app/e2e/sidebar/sidebar-project-collapse.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from "../fixtures" +import { openSidebar, withSession } from "../actions" +import { pawworkSidebarSelector } from "../selectors" + +test("project group folds and unfolds", async ({ page, sdk, gotoSession }) => { + const stamp = Date.now() + await withSession(sdk, `collapse a ${stamp}`, async (a) => { + await withSession(sdk, `collapse b ${stamp}`, async () => { + await gotoSession(a.id) + await openSidebar(page) + + const sidebar = page.locator(pawworkSidebarSelector).first() + + // Switch to project sort so group headers exist. + await sidebar.locator('[data-action="pawwork-sort-trigger"]').click() + await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click() + + const header = sidebar.locator('[data-action="pawwork-group-toggle"]').first() + const content = sidebar.locator('[data-component="pawwork-group-content"]').first() + await expect(header).toBeVisible() + await expect(header).toHaveAttribute("aria-expanded", "true") + await expect(content).not.toHaveAttribute("data-collapsed", "true") + + await header.click() + await expect(header).toHaveAttribute("aria-expanded", "false") + await expect(header).toHaveAttribute("data-collapsed", "true") + // Items stay mounted (focus / scroll preserved); the wrapper is the hide signal. + await expect(content).toHaveAttribute("data-collapsed", "true") + await expect(content).toHaveAttribute("aria-hidden", "true") + // Visible height collapses to 0 via grid-template-rows: 0fr. + await expect.poll(async () => (await content.boundingBox())?.height ?? -1).toBeLessThan(2) + + await header.click() + await expect(header).toHaveAttribute("aria-expanded", "true") + await expect(content).not.toHaveAttribute("data-collapsed", "true") + await expect.poll(async () => (await content.boundingBox())?.height ?? 0).toBeGreaterThan(0) + }) + }) +}) + +test("project collapsed state persists across reload", async ({ page, sdk, gotoSession }) => { + const stamp = Date.now() + await withSession(sdk, `persist collapse ${stamp}`, async (s) => { + await gotoSession(s.id) + await openSidebar(page) + + const sidebar = page.locator(pawworkSidebarSelector).first() + await sidebar.locator('[data-action="pawwork-sort-trigger"]').click() + await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click() + + const header = sidebar.locator('[data-action="pawwork-group-toggle"]').first() + await header.click() + await expect(header).toHaveAttribute("data-collapsed", "true") + + await page.reload() + await openSidebar(page) + const reloaded = page + .locator(pawworkSidebarSelector) + .first() + .locator('[data-action="pawwork-group-toggle"]') + .first() + await expect(reloaded).toBeVisible() + await expect(reloaded).toHaveAttribute("data-collapsed", "true") + await expect(reloaded).toHaveAttribute("aria-expanded", "false") + }) +}) diff --git a/packages/app/e2e/sidebar/sidebar-session-organization.spec.ts b/packages/app/e2e/sidebar/sidebar-session-organization.spec.ts index 0fce14196..813b4124d 100644 --- a/packages/app/e2e/sidebar/sidebar-session-organization.spec.ts +++ b/packages/app/e2e/sidebar/sidebar-session-organization.spec.ts @@ -34,6 +34,7 @@ test("users can pin, rename, and regroup sessions in the PawWork sidebar", async await expect(dialog).toBeHidden() await expect(sidebar.locator(`[data-session-id="${one.id}"]`)).toContainText(`Ops weekly renamed ${stamp}`) - await sidebar.locator('[data-action="pawwork-sort-mode"]').click() + await sidebar.locator('[data-action="pawwork-sort-trigger"]').click() + await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click() await expect(sidebar.locator('[data-component="pawwork-group-header"]')).toHaveCount(1) }) diff --git a/packages/app/e2e/sidebar/sidebar-slice-09.spec.ts b/packages/app/e2e/sidebar/sidebar-slice-09.spec.ts new file mode 100644 index 000000000..5d69a2a4e --- /dev/null +++ b/packages/app/e2e/sidebar/sidebar-slice-09.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from "../fixtures" +import { openSidebar, withSession } from "../actions" +import { pawworkSidebarSelector } from "../selectors" + +test("sidebar row exposes 4-item menu with leading icons", async ({ + page, + sdk, + gotoSession, +}) => { + const stamp = Date.now() + await withSession(sdk, `slice 09 menu ${stamp}`, async (session) => { + await gotoSession(session.id) + await openSidebar(page) + + const sidebar = page.locator(pawworkSidebarSelector).first() + const row = sidebar.locator(`[data-session-id="${session.id}"]`).first() + await row.hover() + await row.locator('[data-action="session-row-menu"]').click() + + const items = page.getByRole("menuitem") + const count = await items.count() + // Export availability depends on runtime config; assert on the stable items. + expect([3, 4]).toContain(count) + const labels = await items.allTextContents() + expect(labels[0]).toMatch(/Pin|置顶/) + expect(labels[1]).toMatch(/Rename|重命名/) + expect(labels[count - 1]).toMatch(/Delete|删除/) + if (count === 4) { + expect(labels[2]).toMatch(/Export|导出/) + } + + // Each item carries a leading icon-svg slot. + for (let i = 0; i < count; i++) { + await expect(items.nth(i).locator('[data-slot="icon-svg"]')).toBeVisible() + } + }) +}) + +test("sort trigger opens a dropdown with two options and reflects the active mode", async ({ page, sdk, gotoSession }) => { + const stamp = Date.now() + await withSession(sdk, `slice 09 sort ${stamp}`, async (session) => { + await gotoSession(session.id) + await openSidebar(page) + + const sidebar = page.locator(pawworkSidebarSelector).first() + const trigger = sidebar.locator('[data-action="pawwork-sort-trigger"]').first() + await expect(trigger).toBeVisible() + await trigger.click() + + const options = page.locator('[data-action="pawwork-sort-option"]') + await expect(options).toHaveCount(2) + await expect(page.locator('[data-action="pawwork-sort-option"][data-value="time"]')).toBeVisible() + await expect(page.locator('[data-action="pawwork-sort-option"][data-value="project"]')).toBeVisible() + + await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click() + await expect(trigger).toHaveAttribute("data-mode", "project") + }) +}) + +test("L37 three-segment shape: side-top, side-scroll, side-foot stacked", async ({ + page, + sdk, + gotoSession, +}) => { + // The side-traffic placeholder is reserved for slice 17 (when traffic-lights + // and collapse control move into the sidebar). Until then, the OS chrome + // already supplies that space, so we ship without the placeholder. + const stamp = Date.now() + await withSession(sdk, `slice 09 shape ${stamp}`, async (session) => { + await gotoSession(session.id) + await openSidebar(page) + + const sidebar = page.locator(pawworkSidebarSelector).first() + const top = sidebar.locator('[data-component="pawwork-side-top"]').first() + const scroll = sidebar.locator('[data-component="pawwork-side-scroll"]').first() + const foot = sidebar.locator('[data-component="pawwork-side-foot"]').first() + + await expect(top).toBeVisible() + await expect(scroll).toBeVisible() + await expect(foot).toBeVisible() + + const topBox = await top.boundingBox() + const scrollBox = await scroll.boundingBox() + const footBox = await foot.boundingBox() + expect(topBox).not.toBeNull() + expect(scrollBox).not.toBeNull() + expect(footBox).not.toBeNull() + expect(topBox!.y).toBeLessThan(scrollBox!.y) + expect(scrollBox!.y).toBeLessThan(footBox!.y) + }) +}) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 5f0352b8f..9ed7f71c5 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -420,7 +420,6 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil ? language.t("session.header.searchFiles") : language.t("palette.search.placeholder"), autofocus: true, - hideIcon: true, }} emptyMessage={language.t("palette.empty")} loadingMessage={language.t("common.loading")} diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index f6bfbbac2..dc868e12d 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1186,7 +1186,7 @@ export const PromptInput: Component = (props) => { size: "normal", style: triggerStyle(), class: - "min-w-0 px-1.5 justify-start! text-13-regular! text-fg-base group", + "min-w-0 px-1.5 justify-start text-13-regular text-fg-base font-normal group", "data-action": "prompt-model", "data-picker-trigger": "", disabled: !actionReady(), @@ -1201,7 +1201,7 @@ export const PromptInput: Component = (props) => { /> {local.model.current()?.name ?? language.t("dialog.model.select.title")} @@ -1251,7 +1251,7 @@ export const PromptInput: Component = (props) => { triggerProps={{ "data-action": "prompt-model-variant", class: - "max-w-[160px] @max-[20rem]/composer:max-w-[80px] text-13-regular! text-fg-base", + "max-w-[160px] @max-[20rem]/composer:max-w-[80px] text-13-regular text-fg-base font-normal", }} /> diff --git a/packages/app/src/components/prompt-input/workspace-chip.tsx b/packages/app/src/components/prompt-input/workspace-chip.tsx index bf46b8758..bb90847f1 100644 --- a/packages/app/src/components/prompt-input/workspace-chip.tsx +++ b/packages/app/src/components/prompt-input/workspace-chip.tsx @@ -48,7 +48,7 @@ export function WorkspaceChip(props: { style?: JSX.CSSProperties | string } = {} "aria-label": language.t("workspace.chip.ariaLabel"), "aria-haspopup": "menu", class: - "px-1.5 inline-flex items-center gap-1.5 text-13-regular text-fg-base", + "px-1.5 inline-flex items-center gap-1.5 text-13-regular text-fg-base font-normal", style: props.style, } as any } diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 19364b835..0e4558d7c 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -391,6 +391,11 @@ export const dict = { "dialog.project.edit.icon.readFailed.title": "Failed to read image", "dialog.project.edit.icon.readFailed.description": "The selected file could not be loaded. Please try another image.", + "dialog.releaseNotes.action.getStarted": "Get started", + "dialog.releaseNotes.action.next": "Next", + "dialog.releaseNotes.action.hideFuture": "Don't show these in the future", + "dialog.releaseNotes.media.alt": "Release preview", + "context.breakdown.title": "Context Breakdown", "context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.', "context.breakdown.system": "System", @@ -804,6 +809,9 @@ export const dict = { "sidebar.pawwork.unpinSession": "Unpin session", "sidebar.pawwork.sort.byProject": "Group by project", "sidebar.pawwork.sort.byTime": "Sort by time", + "sidebar.pawwork.sort.label": "Sort", + "sidebar.pawwork.sort.optionByTime": "By time", + "sidebar.pawwork.sort.optionByProject": "By project", "sidebar.pawwork.searchHistory": "Use Search for older sessions", "debugBar.ariaLabel": "Development performance diagnostics", @@ -830,7 +838,7 @@ export const dict = { "debugBar.mem.tipUnavailable": "Used JS heap vs heap limit. Chromium only.", "debugBar.mem.tip": "Used JS heap vs heap limit. {{used}} of {{limit}}.", - "app.name.desktop": "PawWork Desktop", + "app.name.desktop": "PawWork", "settings.section.desktop": "Desktop", "settings.section.server": "Server", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 76ab43c7f..e083410b0 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -721,6 +721,9 @@ export const dict = { "sidebar.pawwork.unpinSession": "取消置顶", "sidebar.pawwork.sort.byProject": "按项目分组", "sidebar.pawwork.sort.byTime": "按时间排序", + "sidebar.pawwork.sort.label": "排序", + "sidebar.pawwork.sort.optionByTime": "按时间", + "sidebar.pawwork.sort.optionByProject": "按项目", "sidebar.pawwork.searchHistory": "搜索历史会话", "app.name.desktop": "爪印", @@ -1016,6 +1019,10 @@ export const dict = { "workspace.reset.archived.many": "将归档 {{count}} 个会话。", "workspace.reset.note": "这将把工作区重置为与默认分支一致。", "common.open": "打开", + "dialog.releaseNotes.action.getStarted": "开始", + "dialog.releaseNotes.action.next": "下一步", + "dialog.releaseNotes.action.hideFuture": "不再显示", + "dialog.releaseNotes.media.alt": "发布预览", "toast.project.reloadFailed.title": "无法重新加载 {{project}}", "error.server.invalidConfiguration": "配置无效", "common.moreCountSuffix": " (还有 {{count}} 个)", diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 34290e900..395c1f453 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -127,7 +127,14 @@ export default function Layout(props: ParentProps) { const notification = useNotification() const permission = usePermission() const navigate = useNavigate() - setNavigate(navigate) + // Wrap navigate so non-shell entry points (notification clicks, deep links + // dispatched through @/utils/notification-click) also close the settings + // overlay before routing. Shell-driven navigation (openNewSession / + // openSession) closes settings explicitly through closeSettingsSurface. + setNavigate((href) => { + closeSettings() + navigate(href) + }) const providers = useProviders() const dialog = useDialog() const command = useCommand() @@ -1088,6 +1095,14 @@ export default function Layout(props: ParentProps) { setStore("pawworkSortMode", mode) } + function toggleProjectCollapsed(label: string) { + const current = store.pawworkProjectCollapsed + const next: Record = { ...current } + if (next[label]) delete next[label] + else next[label] = true + setStore("pawworkProjectCollapsed", reconcile(next)) + } + // Export hits the embedded sidecar via main-process IPC. When the user has // switched the active server to a remote target, the sidecar holds different // data than the UI; hide the action rather than ship a misleading export. @@ -1446,16 +1461,6 @@ export default function Layout(props: ParentProps) { setSettingsOpen(false) } - let lastSettingsPath: string | undefined - createEffect(() => { - const current = location.pathname - if (settingsOpen()) { - if (lastSettingsPath === undefined) lastSettingsPath = current - else if (lastSettingsPath !== current) closeSettings() - } else { - lastSettingsPath = undefined - } - }) function projectRoot(directory: string) { const key = workspaceKey(directory) @@ -1524,6 +1529,7 @@ export default function Layout(props: ParentProps) { currentProjectRoot: () => currentProject()?.worktree ?? projectRoot(currentDir()), chooseProject, openSettingsSurface, + closeSettingsSurface: closeSettings, }) function openProject(directory: string, shouldNavigate = true) { @@ -2151,6 +2157,8 @@ export default function Layout(props: ParentProps) { activeSessionID={() => params.id} pinnedIDs={() => store.pawworkPinnedSessions} sortMode={() => store.pawworkSortMode} + collapsedProjects={() => store.pawworkProjectCollapsed} + onToggleProjectCollapsed={toggleProjectCollapsed} setScrollContainerRef={workspaceSidebarCtx.setScrollContainerRef} prefetchSession={prefetchSession} onOpenSession={navigateToSession} @@ -2168,6 +2176,8 @@ export default function Layout(props: ParentProps) { onOpenSettings={openSettings} settingsLabel={() => language.t("sidebar.settings")} settingsKeybind={() => command.keybind("settings.open")} + newSessionKeybind={() => command.keybind("session.new")} + searchKeybind={() => command.keybind("command.palette")} /> ) const sidebarContent = () => diff --git a/packages/app/src/pages/layout/layout-page-store.ts b/packages/app/src/pages/layout/layout-page-store.ts index d785409f3..c1ae27987 100644 --- a/packages/app/src/pages/layout/layout-page-store.ts +++ b/packages/app/src/pages/layout/layout-page-store.ts @@ -25,8 +25,21 @@ function pinnedSessions(value: unknown) { }) } +function projectCollapsed(value: unknown) { + if (!record(value)) return {} as Record + const out: Record = {} + for (const [key, val] of Object.entries(value)) { + if (typeof key === "string" && key && val === true) out[key] = true + } + return out +} + function hasLayoutPageFields(value: Record) { - return "pawworkPinnedSessions" in value || "pawworkSortMode" in value + return ( + "pawworkPinnedSessions" in value || + "pawworkSortMode" in value || + "pawworkProjectCollapsed" in value + ) } export function createDefaultLayoutPageState() { @@ -40,6 +53,7 @@ export function createDefaultLayoutPageState() { gettingStartedDismissed: false, pawworkPinnedSessions: [] as string[], pawworkSortMode: "time" as "time" | "project", + pawworkProjectCollapsed: {} as Record, } } @@ -55,6 +69,7 @@ export function migrateLayoutPageState(value: unknown) { ...decoded, pawworkPinnedSessions: pinnedSessions(decoded.pawworkPinnedSessions), pawworkSortMode: decoded.pawworkSortMode === "project" ? "project" : "time", + pawworkProjectCollapsed: projectCollapsed(decoded.pawworkProjectCollapsed), } } if (!hasLayoutPageFields(parsedPage)) return undefined @@ -62,6 +77,7 @@ export function migrateLayoutPageState(value: unknown) { ...parsedPage, pawworkPinnedSessions: pinnedSessions(parsedPage.pawworkPinnedSessions), pawworkSortMode: parsedPage.pawworkSortMode === "project" ? "project" : "time", + pawworkProjectCollapsed: projectCollapsed(parsedPage.pawworkProjectCollapsed), } } @@ -71,6 +87,7 @@ export function migrateLayoutPageState(value: unknown) { ...decoded, pawworkPinnedSessions: pinnedSessions(decoded.pawworkPinnedSessions), pawworkSortMode: decoded.pawworkSortMode === "project" ? "project" : "time", + pawworkProjectCollapsed: projectCollapsed(decoded.pawworkProjectCollapsed), } } diff --git a/packages/app/src/pages/layout/pawwork-sidebar.tsx b/packages/app/src/pages/layout/pawwork-sidebar.tsx index e95cef0a2..f0bea71df 100644 --- a/packages/app/src/pages/layout/pawwork-sidebar.tsx +++ b/packages/app/src/pages/layout/pawwork-sidebar.tsx @@ -4,11 +4,11 @@ import { ContextMenu } from "@opencode-ai/ui/context-menu" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" -import { TooltipKeybind } from "@opencode-ai/ui/tooltip" import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" import { useLanguage } from "@/context/language" import { getRelativeTime } from "@/utils/time" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { DialogRenameSession } from "@/components/dialog-rename-session" import { buildPawworkSessionSections, type PawworkSortMode } from "./pawwork-session-nav" import { buildSessionMenuActions, type SessionMenuAction } from "./session-menu-actions" @@ -22,15 +22,6 @@ export type PawworkSidebarSession = { created: number } -const FilterIcon = (props: { size?: number }) => { - const size = props.size ?? 14 - return ( - - ) -} - export const PawworkSidebar = (props: { scope?: "main" | "peek" sessions: Accessor @@ -39,6 +30,8 @@ export const PawworkSidebar = (props: { activeSessionID?: Accessor pinnedIDs: Accessor sortMode: Accessor + collapsedProjects: Accessor> + onToggleProjectCollapsed: (label: string) => void setScrollContainerRef: (el: HTMLDivElement | undefined) => void prefetchSession: (session: Session, priority?: "high" | "low") => void hrefForSession?: (session: Session) => string @@ -57,6 +50,8 @@ export const PawworkSidebar = (props: { onOpenSettings: () => void settingsLabel: Accessor settingsKeybind: Accessor + newSessionKeybind: Accessor + searchKeybind: Accessor }): JSX.Element => { const language = useLanguage() const dialog = useDialog() @@ -107,7 +102,6 @@ export const PawworkSidebar = (props: { const renderSessionItem = (entry: { item: PawworkSidebarSession }) => { const session = entry.item.session - const isPinned = createMemo(() => props.pinnedIDs().includes(session.id)) const menuLabels = () => ({ pin: language.t("sidebar.pawwork.pinSession"), unpin: language.t("sidebar.pawwork.unpinSession"), @@ -135,6 +129,7 @@ export const PawworkSidebar = (props: { void action.run()}> + {action.label} @@ -151,6 +146,7 @@ export const PawworkSidebar = (props: { void action.run()}> + {action.label} @@ -171,7 +167,6 @@ export const PawworkSidebar = (props: { prefetchSession={props.prefetchSession} hrefForSession={props.hrefForSession} onOpenSession={props.onOpenSession} - pinned={() => isPinned()} timeText={() => entry.item.created > 0 ? getRelativeTime(new Date(entry.item.created).toISOString(), language.t) @@ -179,7 +174,7 @@ export const PawworkSidebar = (props: { } titleContent={({ title }) => ( { e.preventDefault() e.stopPropagation() @@ -195,8 +190,6 @@ export const PawworkSidebar = (props: { as={IconButton} icon="dot-grid" variant="ghost" - - class="rounded-md" data-action="session-row-menu" aria-label={language.t("common.moreOptions")} onClick={(event: MouseEvent) => { @@ -248,40 +241,53 @@ export const PawworkSidebar = (props: { }) }) - const tooltipPlacement = () => "right" as const - const sortAriaLabel = () => - props.sortMode() === "time" ? language.t("sidebar.pawwork.sort.byProject") : language.t("sidebar.pawwork.sort.byTime") - return (
-
+ {/* L37 reserves a top "traffic" segment for slice 17 to fill with the macOS + traffic-light cluster + collapse control. The OS already paints traffic + lights on its window chrome, so reserving a second 32px band here would + double the empty space at the top. The placeholder will return when + slice 17 hides the OS chrome and moves the controls into the sidebar. */} +
- - + + - - - - {language.t("sidebar.pawwork.search")} - + +
@@ -304,36 +310,61 @@ export const PawworkSidebar = (props: { scrollEl = el props.setScrollContainerRef(el) }} - data-component="pawwork-session-scroll" + data-component="pawwork-side-scroll" class="flex-1 min-h-0 overflow-y-auto px-3 pb-3" > 0}> -