Compare commits

...

2 Commits

4 changed files with 193 additions and 63 deletions

View File

@ -62,6 +62,7 @@ The frontend is a workspace package that builds separately:
cd packages/pastebar-app-ui
npm run dev # Development server on port 4422
npm run build # Build to dist-ui/
npm run build:ts # TypeScript check and build
```
### Rust/Tauri Development (src-tauri/)
@ -72,6 +73,21 @@ cargo run --no-default-features # Development mode
cargo build --release # Production build
```
### Code Quality Commands
```bash
# TypeScript type checking (no ESLint configured)
cd packages/pastebar-app-ui && npx tsc --noEmit
# Rust linting
cd src-tauri && cargo clippy
# Format all code
npm run format # Uses Prettier for JS/TS files
```
**Note**: The project currently has no test infrastructure. There are no unit tests, integration tests, or test commands configured.
## Architecture Overview
### High-Level Structure
@ -128,6 +144,26 @@ cargo build --release # Production build
- History window (clipboard history view)
- QuickPaste window (contextual paste menu)
### Tauri Command Patterns
Commands follow consistent patterns in `src-tauri/src/commands/`:
```rust
#[tauri::command]
pub fn command_name(
app_handle: tauri::AppHandle,
state: tauri::State<SomeState>,
params: Type
) -> Result<ReturnType, String> {
// Implementation
}
```
Common state types:
- `tauri::State<Mutex<HashMap<String, Setting>>>` - App settings
- `tauri::State<menu::DbItems>` - Database items
- `tauri::State<menu::DbRecentHistoryItems>` - Recent history
### Database Schema
Main entities:
@ -206,4 +242,16 @@ src-tauri/src/
- Use Diesel migrations for schema changes
- Place migration files in `migrations/` directory
- Run migrations with `npm run diesel:migration:run`
- Run migrations with `npm run diesel:migration:run`
### TypeScript Configuration
- Strict mode enabled in `tsconfig.json`
- Path alias configured: `~/` maps to `packages/pastebar-app-ui/src/`
- Target: ESNext with React JSX transform
### Build Configuration
- Frontend build output: `packages/pastebar-app-ui/dist-ui/`
- Dev server runs on port 4422
- Tauri configs: `tauri.conf.json` (dev) and `tauri.release.conf.json` (production)

View File

@ -72,7 +72,7 @@ const useKeyboardDeleteConfirmation = ({
useHotkeys(
['delete', 'backspace'],
async (e) => {
async e => {
e.preventDefault()
// Only handle keyboard delete when there's a keyboard selected item and no multi-selection

View File

@ -121,6 +121,7 @@ interface ClipboardHistoryRowProps {
clipboard?: ClipboardHistoryItem
isDark: boolean
setRowHeight?: (index: number, height: number) => void
setKeyboardHistorySelectedItemId?: (id: UniqueIdentifier | null) => void
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
setAppFilters?: Dispatch<SetStateAction<string[]>>
isSingleClickToCopyPaste?: boolean
@ -182,6 +183,7 @@ export function ClipboardHistoryRowComponent({
setRowHeight = () => {},
setHistoryFilters = () => {},
setAppFilters = () => {},
setKeyboardHistorySelectedItemId = () => {},
isSingleClickToCopyPaste = false,
historyPreviewLineLimit,
}: ClipboardHistoryRowProps) {
@ -239,10 +241,12 @@ export function ClipboardHistoryRowComponent({
useEffect(() => {
if (isKeyboardSelected && rowKeyboardRef.current && !isScrolling) {
rowKeyboardRef.current.scrollIntoView({
block: 'center',
})
// rowKeyboardRef.current.focus()
if (!isScrolling) {
rowKeyboardRef.current.scrollIntoView({
block: 'nearest',
})
rowKeyboardRef.current.focus({ preventScroll: true })
}
}
}, [isKeyboardSelected, isScrolling])
@ -430,7 +434,7 @@ export function ClipboardHistoryRowComponent({
}
{...(isSelected || isHovering ? listeners : {})}
>
<Box ref={rowRef} tabIndex={0} role="option" aria-selected={isKeyboardSelected}>
<Box ref={rowRef} tabIndex={-1} role="option" aria-selected={isKeyboardSelected}>
{showTimeAgo && (
<Box
className={`flex justify-center text-gray-400 text-xs ${
@ -495,14 +499,14 @@ export function ClipboardHistoryRowComponent({
<Box
className="relative select-none history-item focus:outline-none"
ref={rowKeyboardRef}
tabIndex={0}
tabIndex={-1}
role="option"
aria-selected={isKeyboardSelected}
>
<Box
className={`rounded-md justify-start duration-300 history-box relative px-3 py-1 hover:shadow-sm my-0.5 shadow-none border-2 flex flex-col ${
isKeyboardSelected
? 'ring-2 scale-[.98] ring-blue-400 dark:!ring-blue-600 ring-offset-1 !shadow-sm ring-offset-white dark:ring-offset-gray-800'
isKeyboardSelected
? 'ring-2 scale-[.98] ring-blue-400 dark:!ring-blue-600 ring-offset-1 !shadow-sm ring-offset-white dark:ring-offset-gray-800'
: ''
} ${
index === 0 &&
@ -513,22 +517,22 @@ export function ClipboardHistoryRowComponent({
!isSelected
? 'bg-teal-50 hover:border-slate-300 dark:bg-sky-900/40 dark:hover:border-slate-700 hover:bg-teal-50/90 hover:dark:bg-sky-950'
: (isDeleting || isDeleteConfirmationFromContext.value) &&
!isDragPreview
? 'border-red-400 bg-red-50 dark:bg-red-950/80 dark:border-red-900/80 dark:hover:border-red-800'
!isDragPreview
? 'border-red-400 bg-red-50 dark:bg-red-950/80 dark:border-red-900/80 dark:hover:border-red-800'
: isSelected
? `bg-amber-50 border-amber-300 dark:bg-amber-950/80 dark:border-amber-900/80 hover:border-amber-300/80 dark:hover:border-amber-800 hover:bg-amber-50/80 ${
isPinnedTop ? '!border dark:!bg-amber-950' : ''
}`
: isKeyboardSelected
? `bg-blue-50 border-blue-300 dark:bg-blue-950/80 dark:hover:border-blue-800 hover:bg-blue-50/80 ${
? `bg-blue-50 !border-0 border-blue-300 dark:bg-blue-950/80 dark:hover:border-blue-800 hover:bg-blue-50/80 ${
isPinnedTop ? ' dark:!bg-amber-950' : ''
}`
: contextMenuOpen.value
? 'bg-slate-100 dark:bg-slate-950/80 border-slate-300 dark:border-slate-600'
: isSaved && !isDragPreview
? 'bg-sky-50 border-sky-600 dark:bg-sky-950/80 dark:border-sky-900/80 dark:hover:border-sky-800'
: isCopiedOrPasted && !isDragPreview
? `bg-green-50 border-green-600 dark:bg-green-950/80 dark:border-green-800`
: contextMenuOpen.value
? 'bg-slate-100 dark:bg-slate-950/80 border-slate-300 dark:border-slate-600'
: isSaved && !isDragPreview
? 'bg-sky-50 border-sky-600 dark:bg-sky-950/80 dark:border-sky-900/80 dark:hover:border-sky-800'
: isCopiedOrPasted && !isDragPreview
? `bg-green-50 border-green-600 dark:bg-green-950/80 dark:border-green-800`
: `hover:bg-white dark:hover:bg-slate-950/80 ${
isLargeView
? 'border-slate-500 bg-white dark:bg-slate-950 hover:dark:border-slate-500'
@ -549,16 +553,20 @@ export function ClipboardHistoryRowComponent({
!getSelectedText().text &&
isWindows &&
e.ctrlKey) ||
(e.metaKey && !isWindows)
(e.metaKey &&
!isWindows &&
isSingleClickToCopyPaste &&
!getSelectedText().text)
) {
e.preventDefault()
e.stopPropagation()
onCopyPaste(clipboard.historyId)
} else if ((isWindows && e.ctrlKey) || (e.metaKey && !isWindows)) {
} else if (e.altKey) {
setSelectHistoryItem(clipboard.historyId)
} else if (e.ctrlKey || e.metaKey) {
e.preventDefault()
e.stopPropagation()
setKeyboardHistorySelectedItemId(clipboard.historyId)
} else if (e.shiftKey) {
e.preventDefault()
e.stopPropagation()

View File

@ -35,7 +35,6 @@ import {
showClipsMoveOnBoardId,
showDetailsClipId,
showHistoryDeleteConfirmationId,
showHistoryMultiDeleteConfirmationIds,
showKeyboardNavContextMenuClipId,
showKeyboardNavContextMenuHistoryId,
showLargeViewClipId,
@ -221,8 +220,6 @@ export default function ClipboardHistoryPage() {
const [selectedHistoryItems, setSelectedHistoryItems] = useState<UniqueIdentifier[]>([])
const [showSelectHistoryItems, setShowSelectHistoryItems] = useState(false)
const [isDragPinnedHistory, setIsDragPinnedHistory] = useState(false)
// Use global signal for keyboardSelectedItemId, aliased to avoid conflict if needed locally
// const keyboardSelectedItemId = useSignal<UniqueIdentifier | null>(null); // Removed local signal
const {
isScrolling,
setIsScrolling,
@ -278,37 +275,40 @@ export default function ClipboardHistoryPage() {
},
})
const { showConfirmation: showConfirmationKeyboardDelete, keyboardItemIdDelete, resetTimer: resetKeyboardDeleteTimer } =
useKeyboardDeleteConfirmation({
keyboardSelectedItemId: keyboardSelectedItemId,
selectedHistoryItems,
onConfirmedDelete: async () => {
if (keyboardSelectedItemId.value) {
// Calculate next selection before deletion
const currentIndex = clipboardHistory.findIndex(
item => item.historyId === keyboardSelectedItemId.value
)
let nextSelectedId: UniqueIdentifier | null = null
if (currentIndex !== -1) {
if (currentIndex < clipboardHistory.length - 1) {
// Select next item
nextSelectedId = clipboardHistory[currentIndex + 1].historyId
} else if (currentIndex > 0) {
// Select previous item
nextSelectedId = clipboardHistory[currentIndex - 1].historyId
}
// If only one item, nextSelectedId remains null
const {
showConfirmation: showConfirmationKeyboardDelete,
keyboardItemIdDelete,
resetTimer: resetKeyboardDeleteTimer,
} = useKeyboardDeleteConfirmation({
keyboardSelectedItemId: keyboardSelectedItemId,
selectedHistoryItems,
onConfirmedDelete: async () => {
if (keyboardSelectedItemId.value) {
// Calculate next selection before deletion
const currentIndex = clipboardHistory.findIndex(
item => item.historyId === keyboardSelectedItemId.value
)
let nextSelectedId: UniqueIdentifier | null = null
if (currentIndex !== -1) {
if (currentIndex < clipboardHistory.length - 1) {
// Select next item
nextSelectedId = clipboardHistory[currentIndex + 1].historyId
} else if (currentIndex > 0) {
// Select previous item
nextSelectedId = clipboardHistory[currentIndex - 1].historyId
}
await deleteClipboardHistoryByIds({
historyIds: [keyboardSelectedItemId.value],
})
// Update selection to the calculated next item
keyboardSelectedItemId.value = nextSelectedId
// If only one item, nextSelectedId remains null
}
},
})
await deleteClipboardHistoryByIds({
historyIds: [keyboardSelectedItemId.value],
})
// Update selection to the calculated next item
keyboardSelectedItemId.value = nextSelectedId
}
},
})
const isPinnedPanelHoverOpen = useMemo(() => {
return isPinnedPanelKeepOpen.value || isPinnedPanelHovering.value
@ -508,6 +508,8 @@ export default function ClipboardHistoryPage() {
},
{
enabled: !shouldKeyboardNavigationBeDisabled.value,
enableOnFormTags: false,
preventDefault: true,
}
)
@ -698,7 +700,10 @@ export default function ClipboardHistoryPage() {
useHotkeys(
['space'],
() => {
e => {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
if (
currentNavigationContext.value === 'history' ||
currentNavigationContext.value === null
@ -706,7 +711,7 @@ export default function ClipboardHistoryPage() {
if (keyboardSelectedItemId.value) {
// Reset keyboard delete confirmation when selecting
resetKeyboardDeleteTimer()
setSelectHistoryItem(keyboardSelectedItemId.value)
const currentItemIndex = clipboardHistory.findIndex(
item => item.historyId === keyboardSelectedItemId.value
@ -723,6 +728,8 @@ export default function ClipboardHistoryPage() {
},
{
enabled: !shouldKeyboardNavigationBeDisabled.value,
enableOnFormTags: false,
preventDefault: true,
}
)
@ -741,7 +748,7 @@ export default function ClipboardHistoryPage() {
// Reset delete confirmation on escape
showHistoryDeleteConfirmationId.value = null
// Reset keyboard delete confirmation on escape
resetKeyboardDeleteTimer()
@ -769,6 +776,8 @@ export default function ClipboardHistoryPage() {
['arrowdown'],
e => {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
// Clear any delete timeout when navigating away
if (deleteTimeoutRef.current) {
@ -778,7 +787,7 @@ export default function ClipboardHistoryPage() {
// Reset delete confirmation when navigating to a different item
showHistoryDeleteConfirmationId.value = null
// Reset keyboard delete confirmation when navigating
resetKeyboardDeleteTimer()
@ -798,6 +807,8 @@ export default function ClipboardHistoryPage() {
(currentNavigationContext.value === 'history' ||
currentNavigationContext.value === null) &&
!shouldKeyboardNavigationBeDisabled.value,
enableOnFormTags: false,
preventDefault: true,
}
)
@ -805,6 +816,8 @@ export default function ClipboardHistoryPage() {
['arrowup'],
e => {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
// Clear any delete timeout when navigating away
if (deleteTimeoutRef.current) {
@ -814,7 +827,7 @@ export default function ClipboardHistoryPage() {
// Reset delete confirmation when navigating to a different item
showHistoryDeleteConfirmationId.value = null
// Reset keyboard delete confirmation when navigating
resetKeyboardDeleteTimer()
@ -840,6 +853,8 @@ export default function ClipboardHistoryPage() {
(currentNavigationContext.value === 'history' ||
currentNavigationContext.value === null) &&
!shouldKeyboardNavigationBeDisabled.value,
enableOnFormTags: false,
preventDefault: true,
}
)
@ -873,6 +888,8 @@ export default function ClipboardHistoryPage() {
},
{
enabled: !shouldKeyboardNavigationBeDisabled.value,
enableOnFormTags: false,
preventDefault: true,
}
)
@ -906,6 +923,8 @@ export default function ClipboardHistoryPage() {
},
{
enabled: !shouldKeyboardNavigationBeDisabled.value,
enableOnFormTags: false,
preventDefault: true,
}
)
@ -913,6 +932,8 @@ export default function ClipboardHistoryPage() {
['home'],
e => {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
if (
currentNavigationContext.value === 'history' ||
currentNavigationContext.value === null
@ -958,6 +979,8 @@ export default function ClipboardHistoryPage() {
},
{
enabled: !shouldKeyboardNavigationBeDisabled.value,
enableOnFormTags: false,
preventDefault: true,
}
)
@ -965,6 +988,8 @@ export default function ClipboardHistoryPage() {
['pageup'],
e => {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
if (
currentNavigationContext.value === 'history' ||
currentNavigationContext.value === null
@ -1005,6 +1030,8 @@ export default function ClipboardHistoryPage() {
},
{
enabled: !shouldKeyboardNavigationBeDisabled.value,
enableOnFormTags: false,
preventDefault: true,
}
)
@ -1012,6 +1039,8 @@ export default function ClipboardHistoryPage() {
['pagedown'],
e => {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
if (
currentNavigationContext.value === 'history' ||
currentNavigationContext.value === null
@ -1052,6 +1081,8 @@ export default function ClipboardHistoryPage() {
},
{
enabled: !shouldKeyboardNavigationBeDisabled.value,
enableOnFormTags: false,
preventDefault: true,
}
)
@ -1076,9 +1107,45 @@ export default function ClipboardHistoryPage() {
},
{
enabled: !shouldKeyboardNavigationBeDisabled.value,
enableOnFormTags: false,
preventDefault: true,
}
)
function setKeyboardHistorySelectedItemId(itemId: UniqueIdentifier | null) {
if (itemId) {
if (deleteTimeoutRef.current) {
clearTimeout(deleteTimeoutRef.current)
deleteTimeoutRef.current = null
}
if (itemId === keyboardSelectedItemId.value) {
resetKeyboardNavigation()
return
}
// Reset delete confirmation when navigating to a different item
showHistoryDeleteConfirmationId.value = null
// Reset keyboard delete confirmation when navigating
resetKeyboardDeleteTimer()
const currentItemIndex = clipboardHistory.findIndex(
item => item.historyId === itemId
)
if (currentItemIndex === -1) {
keyboardSelectedItemId.value = null
return
}
currentNavigationContext.value = 'history'
keyboardSelectedItemId.value = itemId
} else {
keyboardSelectedItemId.value = null
}
}
// Store timeout reference to clear it if needed
const deleteTimeoutRef = useRef<NodeJS.Timeout | null>(null)
@ -1347,15 +1414,19 @@ export default function ClipboardHistoryPage() {
const hasIsDeleting = (historyId: UniqueIdentifier) => {
return (
// Keyboard delete confirmation - only for the specific keyboard selected item
(showConfirmationKeyboardDelete &&
historyId === keyboardItemIdDelete &&
historyId === keyboardSelectedItemId.value) ||
(showConfirmationKeyboardDelete &&
historyId === keyboardItemIdDelete &&
historyId === keyboardSelectedItemId.value) ||
// Mouse delete confirmation - only when keyboard delete is NOT active
(showConfirmation && !showConfirmationKeyboardDelete && selectedHistoryItems.includes(historyId)) ||
(showConfirmation &&
!showConfirmationKeyboardDelete &&
selectedHistoryItems.includes(historyId)) ||
// Single item delete confirmation
historyId === showHistoryDeleteConfirmationId.value ||
// Hovering delete confirmation - only when keyboard delete is NOT active
(showConfirmation && !showConfirmationKeyboardDelete && historyId === hoveringHistoryIdDelete) ||
(showConfirmation &&
!showConfirmationKeyboardDelete &&
historyId === hoveringHistoryIdDelete) ||
// Drag over trash
historyId === dragOverTrashId ||
(Boolean(dragOverTrashId) &&
@ -2513,6 +2584,9 @@ export default function ClipboardHistoryPage() {
historyPreviewLineLimit
}
index={index}
setKeyboardHistorySelectedItemId={
setKeyboardHistorySelectedItemId
}
style={style}
/>
)