feat: inherit model-picker reorder/accordion persistence and shift-delete from otto-ui (#1833)
* feat(model-picker): drag-to-reorder providers and persist accordion state Ports two model/provider picker QoL features from otto-ui: - Persist collapsed state of picker sections (favorites, recent, each provider) via a new persisted zustand store so collapse survives remounts and reloads, shared across every picker surface. - Desktop drag-to-reorder of provider sections (whole header as the mouse activator, 8px threshold so a plain click still toggles collapse), with the order persisted in useUIStore.providerOrder and applied across ModelControls, ModelMultiSelect and ModelSelector. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * feat(sessions): shift-click quick action hard-deletes thread without prompt Ports the shift-quick-delete feature from otto-ui. The sidebar quick action (normally archive) becomes a no-prompt hard delete while Shift is held: the icon switches to a trash bin, the affordance turns destructive, and the click bypasses the confirmation dialog via a new skipConfirm source flag. A shared useShiftKeyHeld hook (single window listener set, useSyncExternalStore) keeps only the small action button re-rendering on Shift state changes, and resets on window blur so the affordance can't get stuck after alt-tab. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> --------- Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
Serhii Dziupin
parent
e7b952cbcb
commit
3e3cd82a47
@@ -0,0 +1,77 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
/**
|
||||
* Tracks whether the Shift key is currently held, shared across all consumers
|
||||
* via a single set of window listeners.
|
||||
*
|
||||
* Using one module-level listener set (instead of per-component listeners)
|
||||
* keeps this cheap even when many rows subscribe, and `useSyncExternalStore`
|
||||
* lets unrelated subtrees stay isolated — only components that actually call
|
||||
* this hook re-render when the Shift state flips.
|
||||
*/
|
||||
let shiftHeld = false;
|
||||
const listeners = new Set<() => void>();
|
||||
let initialized = false;
|
||||
|
||||
function emit(): void {
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function setShiftHeld(next: boolean): void {
|
||||
if (shiftHeld === next) {
|
||||
return;
|
||||
}
|
||||
shiftHeld = next;
|
||||
emit();
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent): void {
|
||||
if (event.key === 'Shift') {
|
||||
setShiftHeld(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent): void {
|
||||
if (event.key === 'Shift') {
|
||||
setShiftHeld(false);
|
||||
}
|
||||
}
|
||||
|
||||
// The window can lose focus while Shift is held (e.g. alt-tab), and the
|
||||
// matching keyup never arrives — reset so the UI doesn't get stuck in the
|
||||
// "delete" affordance.
|
||||
function handleReset(): void {
|
||||
setShiftHeld(false);
|
||||
}
|
||||
|
||||
function ensureListeners(): void {
|
||||
if (initialized || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
window.addEventListener('blur', handleReset);
|
||||
}
|
||||
|
||||
function subscribe(onStoreChange: () => void): () => void {
|
||||
ensureListeners();
|
||||
listeners.add(onStoreChange);
|
||||
return () => {
|
||||
listeners.delete(onStoreChange);
|
||||
};
|
||||
}
|
||||
|
||||
function getSnapshot(): boolean {
|
||||
return shiftHeld;
|
||||
}
|
||||
|
||||
function getServerSnapshot(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function useShiftKeyHeld(): boolean {
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
}
|
||||
Reference in New Issue
Block a user