feat(desktop): drag-to-resize columns, double-click reset, title wrap (#119)#125
Conversation
…legeling#119) Three small usability wins asked for in issue legeling#119, landed together: 1. **Drag-to-resize sidebar columns.** The folder sidebar (`<aside>` panel) and the card-view prompt list pane both had hard-coded Tailwind widths (`w-72` / `w-80`), which wasted screen real estate on 4K displays. They are now fully resizable via a vertical drag handle and persist across sessions via the existing Zustand localStorage. The handles clamp to safe bounds so a runaway drag can't hide the UI. 2. **Double-click / keyboard reset.** Double-clicking the drag handle restores the column to its default width. Keyboard users can nudge with ArrowLeft / ArrowRight (Shift for 64 px steps) and reset with Enter / Space / Home / End. Fully ARIA-labelled as `role="separator" aria-orientation="vertical"` with live `aria-valuenow` for assistive tech. 3. **Title auto-wrap in list/card/gallery views.** Prompt titles in PromptCard (the card-view list pane), PromptListView, and PromptGalleryView previously used `truncate`, so titles longer than the column were silently cut off. They now use `line-clamp-2 break-words` so users see a second line before ellipsis, with the full title still available via the `title=""` tooltip. ### Architecture - Added a reusable `<ColumnResizer />` primitive under `components/ui/` so future panes can opt in trivially. - Added `sidebarPanelWidth` / `promptListPaneWidth` to `useUIStore` with a `merge` function that re-clamps out-of-range persisted values, so older / future schema drift cannot leave the layout stuck in a broken state. ### Tests - `tests/unit/stores/ui-columns.test.ts` (8 tests): defaults match the legacy Tailwind widths, clamping on both ends, NaN / Infinity reject to min (defensive), reset round-trips, and the persist `merge` repair for out-of-range localStorage values. - `tests/unit/components/column-resizer.test.tsx` (7 tests): pointer drag math, clamp during drag, primary-button filtering, double-click reset, keyboard step + reset, and ARIA exposure. ### Verification - `pnpm lint` → clean - `pnpm test -- --run tests/unit/components/column-resizer.test.tsx tests/unit/stores/ui-columns.test.ts` → 15/15 passing ### Notes - legeling#113 (card-view inline edit) was already addressed by a prior change in this repo — see the card-detail-inline-edit change folder. - No IPC, schema, or data migration changes. Refs legeling#119
|
@TuTouPower is attempting to deploy a commit to the legeling's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (5)
📝 WalkthroughWalkthrough该 PR 为桌面端添加持久化的可调整列宽(侧边栏与提示列表),引入 ColumnResizer 组件并在 MainContent 与 Sidebar 中集成;同时将提示标题/描述改为两行显示并新增相关多语言 ARIA 字符串与单元测试。 变更可调整列宽和文本包装
Sequence Diagram(s)sequenceDiagram
participant User
participant ColumnResizer
participant UIStore
participant Layout (MainContent/Sidebar)
User->>ColumnResizer: pointerDown / drag / keyDown / doubleClick
ColumnResizer->>ColumnResizer: compute delta & clamp
ColumnResizer->>UIStore: onResize(clampedWidth)
UIStore->>Layout: provide updated width (rehydrate/persist)
Layout->>User: updated pane width applied (CSS var)
🎯 3 (Moderate) | ⏱️ ~25 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review Summary by QodoAdd drag-to-resize columns, double-click reset, and title wrapping
WalkthroughsDescription• Add drag-to-resize functionality for sidebar and prompt list panes - Vertical drag handles with pointer, keyboard, and double-click support - Widths persist across sessions via Zustand localStorage with clamping • Replace truncated titles with line-clamp-2 for better visibility - Applied to PromptCard, PromptListView, and PromptGalleryView - Full titles available via tooltip attribute • New reusable ColumnResizer component for future resizable panes - ARIA-labeled with live aria-valuenow for accessibility - Supports ArrowLeft/ArrowRight (Shift for 64px steps) and reset keys • Add comprehensive unit tests for store and component behavior - 8 tests for column width clamping and persistence merge logic - 7 tests for pointer drag, keyboard, double-click, and ARIA attributes Diagramflowchart LR
A["User drags handle"] --> B["ColumnResizer calculates delta"]
B --> C["Clamp to min/max bounds"]
C --> D["Call onResize callback"]
D --> E["Update store width"]
E --> F["Persist to localStorage"]
G["Double-click handle"] --> H["Reset to defaultWidth"]
H --> D
I["Keyboard ArrowLeft/Right"] --> J["Step 16px or 64px with Shift"]
J --> D
K["Truncated titles"] --> L["Replace with line-clamp-2"]
L --> M["Show 2 lines before ellipsis"]
File Changes1. apps/desktop/src/renderer/stores/ui.store.ts
|
Code Review by Qodo
1.
|
| const merged = { ...current, ...(persisted as Partial<UIState>) }; | ||
| return { | ||
| ...merged, | ||
| sidebarPanelWidth: clamp( | ||
| merged.sidebarPanelWidth ?? SIDEBAR_PANEL_WIDTH_DEFAULT, | ||
| SIDEBAR_PANEL_WIDTH_MIN, | ||
| SIDEBAR_PANEL_WIDTH_MAX, | ||
| ), | ||
| promptListPaneWidth: clamp( | ||
| merged.promptListPaneWidth ?? PROMPT_LIST_PANE_WIDTH_DEFAULT, | ||
| PROMPT_LIST_PANE_WIDTH_MIN, | ||
| PROMPT_LIST_PANE_WIDTH_MAX, | ||
| ), | ||
| } as UIState; | ||
| }, |
There was a problem hiding this comment.
4. Unjustified as assertions 📘 Rule violation ⚙ Maintainability
The PR introduces as type assertions without an explanatory justification comment, which violates the casting restriction rule. This can hide real typing issues and reduce type safety guarantees.
Agent Prompt
## Issue description
New `as` assertions were introduced without justification comments.
## Issue Context
Compliance requires either removing unnecessary assertions or documenting why the cast is required (interop/unsafe boundary).
## Fix Focus Areas
- apps/desktop/src/renderer/stores/ui.store.ts[108-122]
- apps/desktop/src/renderer/components/ui/ColumnResizer.tsx[107-109]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| <ColumnResizer | ||
| currentWidth={sidebarPanelWidth} | ||
| min={SIDEBAR_PANEL_WIDTH_MIN} | ||
| max={SIDEBAR_PANEL_WIDTH_MAX} | ||
| defaultWidth={SIDEBAR_PANEL_WIDTH_DEFAULT} | ||
| onResize={setSidebarPanelWidth} | ||
| ariaLabel={t('sidebar.resizeAria', 'Resize folder sidebar')} | ||
| /> |
There was a problem hiding this comment.
5. Hardcoded aria label fallback 📘 Rule violation ⚙ Maintainability
A user-facing ARIA label fallback string is hardcoded in renderer code instead of relying solely on i18next keys. This violates the requirement that all user-visible text be produced via t() with translation resources (no hardcoded strings).
Agent Prompt
## Issue description
The ARIA label uses `t('...', '...')` with a hardcoded fallback string, which violates the no-hardcoded-strings i18n requirement.
## Issue Context
ARIA labels are user-facing (assistive tech) and must be localized.
## Fix Focus Areas
- apps/desktop/src/renderer/components/layout/Sidebar.tsx[1420-1427]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/desktop/src/renderer/stores/ui.store.ts`:
- Around line 107-122: The two type assertions inside the merge function (the
cast of persisted to Partial<UIState> and the final cast to UIState) need brief
explanatory comments: add an inline comment next to the persisted cast
explaining that the persist middleware provides an unknown type and we must
assert to Partial<UIState> for safe spread during merge, and add an inline
comment next to the final "as UIState" explaining that TypeScript cannot
statically infer the fully populated UIState after applying defaults/clamps so
an assertion is required to satisfy the persist API; keep the comments concise
and reference the interplay with Zustand persist and UIState so reviewers
understand why the asserts are necessary.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 32504a1d-88f5-47a1-9fb0-1e87c09d8b38
📒 Files selected for processing (15)
apps/desktop/src/renderer/components/layout/MainContent.tsxapps/desktop/src/renderer/components/layout/Sidebar.tsxapps/desktop/src/renderer/components/prompt/PromptGalleryView.tsxapps/desktop/src/renderer/components/prompt/PromptListView.tsxapps/desktop/src/renderer/components/ui/ColumnResizer.tsxapps/desktop/src/renderer/i18n/locales/de.jsonapps/desktop/src/renderer/i18n/locales/en.jsonapps/desktop/src/renderer/i18n/locales/es.jsonapps/desktop/src/renderer/i18n/locales/fr.jsonapps/desktop/src/renderer/i18n/locales/ja.jsonapps/desktop/src/renderer/i18n/locales/zh-TW.jsonapps/desktop/src/renderer/i18n/locales/zh.jsonapps/desktop/src/renderer/stores/ui.store.tsapps/desktop/tests/unit/components/column-resizer.test.tsxapps/desktop/tests/unit/stores/ui-columns.test.ts
| merge: (persisted, current) => { | ||
| const merged = { ...current, ...(persisted as Partial<UIState>) }; | ||
| return { | ||
| ...merged, | ||
| sidebarPanelWidth: clamp( | ||
| merged.sidebarPanelWidth ?? SIDEBAR_PANEL_WIDTH_DEFAULT, | ||
| SIDEBAR_PANEL_WIDTH_MIN, | ||
| SIDEBAR_PANEL_WIDTH_MAX, | ||
| ), | ||
| promptListPaneWidth: clamp( | ||
| merged.promptListPaneWidth ?? PROMPT_LIST_PANE_WIDTH_DEFAULT, | ||
| PROMPT_LIST_PANE_WIDTH_MIN, | ||
| PROMPT_LIST_PANE_WIDTH_MAX, | ||
| ), | ||
| } as UIState; | ||
| }, |
There was a problem hiding this comment.
为类型断言添加说明注释
merge 函数中的两处类型断言(第 108 行和第 121 行)缺少解释性注释。根据编码规范,类型断言仅在与外部库互操作时必要,且必须附带注释说明原因。
这两处断言对于 Zustand 的 persist 中间件类型是必需的:
- 第 108 行:
persisted的类型是unknown,需要断言为Partial<UIState>才能安全展开 - 第 121 行:TypeScript 无法推断展开操作后的对象完全满足
UIState接口
建议添加的注释
merge: (persisted, current) => {
+ // Zustand persist provides persisted state as `unknown`; cast to Partial
+ // to safely merge with current state.
const merged = { ...current, ...(persisted as Partial<UIState>) };
return {
...merged,
sidebarPanelWidth: clamp(
merged.sidebarPanelWidth ?? SIDEBAR_PANEL_WIDTH_DEFAULT,
SIDEBAR_PANEL_WIDTH_MIN,
SIDEBAR_PANEL_WIDTH_MAX,
),
promptListPaneWidth: clamp(
merged.promptListPaneWidth ?? PROMPT_LIST_PANE_WIDTH_DEFAULT,
PROMPT_LIST_PANE_WIDTH_MIN,
PROMPT_LIST_PANE_WIDTH_MAX,
),
+ // TypeScript cannot verify that the spread + explicit fields cover all
+ // UIState properties; assertion is safe because we spread current (the
+ // full default state) first.
} as UIState;
},根据编码规范:"Type assertions with as are prohibited unless truly necessary for interop, and must include a comment explaining why"
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| merge: (persisted, current) => { | |
| const merged = { ...current, ...(persisted as Partial<UIState>) }; | |
| return { | |
| ...merged, | |
| sidebarPanelWidth: clamp( | |
| merged.sidebarPanelWidth ?? SIDEBAR_PANEL_WIDTH_DEFAULT, | |
| SIDEBAR_PANEL_WIDTH_MIN, | |
| SIDEBAR_PANEL_WIDTH_MAX, | |
| ), | |
| promptListPaneWidth: clamp( | |
| merged.promptListPaneWidth ?? PROMPT_LIST_PANE_WIDTH_DEFAULT, | |
| PROMPT_LIST_PANE_WIDTH_MIN, | |
| PROMPT_LIST_PANE_WIDTH_MAX, | |
| ), | |
| } as UIState; | |
| }, | |
| merge: (persisted, current) => { | |
| // Zustand persist provides persisted state as `unknown`; cast to Partial | |
| // to safely merge with current state. | |
| const merged = { ...current, ...(persisted as Partial<UIState>) }; | |
| return { | |
| ...merged, | |
| sidebarPanelWidth: clamp( | |
| merged.sidebarPanelWidth ?? SIDEBAR_PANEL_WIDTH_DEFAULT, | |
| SIDEBAR_PANEL_WIDTH_MIN, | |
| SIDEBAR_PANEL_WIDTH_MAX, | |
| ), | |
| promptListPaneWidth: clamp( | |
| merged.promptListPaneWidth ?? PROMPT_LIST_PANE_WIDTH_DEFAULT, | |
| PROMPT_LIST_PANE_WIDTH_MIN, | |
| PROMPT_LIST_PANE_WIDTH_MAX, | |
| ), | |
| // TypeScript cannot verify that the spread + explicit fields cover all | |
| // UIState properties; assertion is safe because we spread current (the | |
| // full default state) first. | |
| } as UIState; | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/desktop/src/renderer/stores/ui.store.ts` around lines 107 - 122, The two
type assertions inside the merge function (the cast of persisted to
Partial<UIState> and the final cast to UIState) need brief explanatory comments:
add an inline comment next to the persisted cast explaining that the persist
middleware provides an unknown type and we must assert to Partial<UIState> for
safe spread during merge, and add an inline comment next to the final "as
UIState" explaining that TypeScript cannot statically infer the fully populated
UIState after applying defaults/clamps so an assertion is required to satisfy
the persist API; keep the comments concise and reference the interplay with
Zustand persist and UIState so reviewers understand why the asserts are
necessary.
- Drop Chinese comment lines introduced by this PR (AGENTS.md rule)
- Replace direct inline width styles with Tailwind arbitrary utilities
that read from a single CSS custom property. Runtime-dynamic widths
now land in the DOM as `w-[var(--sidebar-panel-width)]` /
`w-[var(--prompt-list-pane-width)]`, with the pixel value supplied
via a CSS variable instead of a direct `style={{ width: n }}`. This
is Tailwind's recommended pattern for dynamic sizing and keeps the
visible utilities in the class string.
- ColumnResizer's hit area is now expressed purely in Tailwind
(`w-2 shrink-0 touch-none`) — no more static-value inline style.
- Replace the two empty catch blocks in ColumnResizer with
`console.warn` / `console.debug` so pointer-capture unavailability
is no longer invisible to support traces.
Summary
Three small usability wins asked for in #119, landed together.
Drag-to-resize sidebar columns. The folder sidebar and the card-view prompt list pane had hard-coded Tailwind widths (`w-72` / `w-80`), which wasted screen real estate on 4K displays. They are now fully resizable via a vertical drag handle and persist across sessions via the existing Zustand `localStorage`. Widths clamp to safe bounds so a runaway drag cannot hide the UI.
Double-click / keyboard reset. Double-clicking the drag handle restores the column to its default width. Keyboard users can nudge with ArrowLeft / ArrowRight (Shift = 64 px steps) and reset with Enter / Space / Home / End. Fully ARIA-labelled as `role="separator" aria-orientation="vertical"` with live `aria-valuenow` for assistive tech.
Title auto-wrap in list / card / gallery views. Prompt titles in PromptCard (card-view list pane), PromptListView, and PromptGalleryView previously used `truncate`, so titles longer than the column were silently cut off. They now use `line-clamp-2 break-words` — users see a second line before ellipsis, with the full title still available via the `title=""` tooltip.
Architecture
Tests
Verification
Notes
Refs #119
Summary by CodeRabbit
新功能
测试