mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Panel: add peeking support to the store
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
// configuration
|
||||
export const OPTIMA_DRAWER_BACKGROUND = 'var(--joy-palette-background-popup)';
|
||||
export const OPTIMA_DRAWER_MOBILE_RADIUS = 'var(--joy-radius-lg)';
|
||||
export const OPTIMA_DRAWER_HOVER_TIMEOUT = 300; // ms - exit delay: time to hide after mouse leaves
|
||||
export const OPTIMA_DRAWER_HOVER_ENTER_DELAY = 200; // ms - enter delay: prevents accidental triggers
|
||||
export const OPTIMA_DRAWER_OPEN_DEBOUNCE = 100; // ms
|
||||
export const OPTIMA_OPEN_DEBOUNCE = 100; // ms - prevent accidental immediate close
|
||||
export const OPTIMA_PEEK_HOVER_ENTER_DELAY = 200; // ms - enter delay: prevents accidental triggers
|
||||
export const OPTIMA_PEEK_HOVER_TIMEOUT = 300; // ms - exit delay: time to hide after mouse leaves
|
||||
export const OPTIMA_NAV_RADIUS = 'sm';
|
||||
export const OPTIMA_PANEL_GROUPS_SPACING = 2.5;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { isBrowser } from '~/common/util/pwaUtils';
|
||||
import { navItems } from '~/common/app.nav';
|
||||
|
||||
import { OPTIMA_DRAWER_HOVER_ENTER_DELAY, OPTIMA_DRAWER_HOVER_TIMEOUT, OPTIMA_DRAWER_OPEN_DEBOUNCE } from './optima.config';
|
||||
import { OPTIMA_OPEN_DEBOUNCE, OPTIMA_PEEK_HOVER_ENTER_DELAY, OPTIMA_PEEK_HOVER_TIMEOUT } from './optima.config';
|
||||
|
||||
|
||||
export type PreferencesTabId = 'chat' | 'voice' | 'draw' | 'tools' | undefined;
|
||||
@@ -20,6 +20,7 @@ interface OptimaState {
|
||||
drawerIsOpen: boolean;
|
||||
drawerIsPeeking: boolean;
|
||||
panelIsOpen: boolean;
|
||||
panelIsPeeking: boolean;
|
||||
|
||||
// modals
|
||||
showAIXDebugger: boolean;
|
||||
@@ -63,6 +64,7 @@ const initialState: OptimaState = {
|
||||
drawerIsOpen: initialDrawerOpen(),
|
||||
drawerIsPeeking: false,
|
||||
panelIsOpen: false,
|
||||
panelIsPeeking: false,
|
||||
|
||||
// modals that can overlay anything
|
||||
...modalsClosedState,
|
||||
@@ -86,6 +88,8 @@ export interface OptimaActions {
|
||||
closePanel: () => void;
|
||||
openPanel: () => void;
|
||||
togglePanel: () => void;
|
||||
peekPanelEnter: () => void;
|
||||
peekPanelLeave: () => void;
|
||||
|
||||
closeAIXDebugger: () => void;
|
||||
openAIXDebugger: () => void;
|
||||
@@ -108,19 +112,50 @@ export interface OptimaActions {
|
||||
}
|
||||
|
||||
|
||||
// global state: peek hover logic timers
|
||||
let peekEnterTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
let peekLeaveTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
const drawerPeek = createPeekHandlers('drawerIsOpen', 'drawerIsPeeking');
|
||||
const panelPeek = createPeekHandlers('panelIsOpen', 'panelIsPeeking');
|
||||
|
||||
function cancelPeekTimers(which?: 'enter' | 'leave') {
|
||||
if (which !== 'leave' && peekEnterTimeoutId) {
|
||||
clearTimeout(peekEnterTimeoutId);
|
||||
peekEnterTimeoutId = null;
|
||||
}
|
||||
if (which !== 'enter' && peekLeaveTimeoutId) {
|
||||
clearTimeout(peekLeaveTimeoutId);
|
||||
peekLeaveTimeoutId = null;
|
||||
}
|
||||
function createPeekHandlers<
|
||||
TOpenKey extends keyof OptimaState,
|
||||
TPeekingKey extends keyof OptimaState,
|
||||
>(isOpenKey: TOpenKey, isPeekingKey: TPeekingKey, overrideEnterDelay?: number) {
|
||||
let enterTimer: any = null;
|
||||
let leaveTimer: any = null;
|
||||
|
||||
return {
|
||||
cancel: () => {
|
||||
clearTimeout(enterTimer);
|
||||
clearTimeout(leaveTimer);
|
||||
enterTimer = null;
|
||||
leaveTimer = null;
|
||||
},
|
||||
enter: (_get: () => OptimaState, _set: (state: Partial<OptimaState>) => void) => {
|
||||
clearTimeout(leaveTimer);
|
||||
leaveTimer = null;
|
||||
|
||||
const state = _get();
|
||||
if (state[isOpenKey] || state[isPeekingKey]) return;
|
||||
|
||||
clearTimeout(enterTimer);
|
||||
enterTimer = setTimeout(() => {
|
||||
_set({ [isPeekingKey]: true } as Partial<OptimaState>);
|
||||
enterTimer = null;
|
||||
}, overrideEnterDelay ?? OPTIMA_PEEK_HOVER_ENTER_DELAY);
|
||||
},
|
||||
leave: (_get: () => OptimaState, _set: (state: Partial<OptimaState>) => void) => {
|
||||
clearTimeout(enterTimer);
|
||||
enterTimer = null;
|
||||
|
||||
const state = _get();
|
||||
if (!state[isPeekingKey]) return;
|
||||
|
||||
clearTimeout(leaveTimer);
|
||||
leaveTimer = setTimeout(() => {
|
||||
_set({ [isPeekingKey]: false } as Partial<OptimaState>);
|
||||
leaveTimer = null;
|
||||
}, OPTIMA_PEEK_HOVER_TIMEOUT);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -132,53 +167,31 @@ export const useLayoutOptimaStore = create<OptimaState & OptimaActions>((_set, _
|
||||
|
||||
closeDrawer: () => {
|
||||
// prevent accidental immediate close (e.g. double-click, animation protection)
|
||||
if (Date.now() - _get().lastDrawerOpenTime < OPTIMA_DRAWER_OPEN_DEBOUNCE) return;
|
||||
cancelPeekTimers();
|
||||
if (Date.now() - _get().lastDrawerOpenTime < OPTIMA_OPEN_DEBOUNCE) return;
|
||||
drawerPeek.cancel();
|
||||
_set({ drawerIsOpen: false, drawerIsPeeking: false });
|
||||
},
|
||||
openDrawer: () => {
|
||||
cancelPeekTimers();
|
||||
drawerPeek.cancel();
|
||||
_set({ drawerIsOpen: true, drawerIsPeeking: false, lastDrawerOpenTime: Date.now() });
|
||||
},
|
||||
toggleDrawer: () => _get().drawerIsOpen ? _get().closeDrawer() : _get().openDrawer(),
|
||||
|
||||
peekDrawerEnter: () => {
|
||||
cancelPeekTimers('leave');
|
||||
|
||||
// if drawer is already open, no need to peek
|
||||
const { drawerIsOpen, drawerIsPeeking } = _get();
|
||||
if (drawerIsOpen || drawerIsPeeking) return;
|
||||
|
||||
// start a new timer to show the drawer after a small delay
|
||||
cancelPeekTimers('enter');
|
||||
peekEnterTimeoutId = setTimeout(() => {
|
||||
_set({ drawerIsPeeking: true });
|
||||
peekEnterTimeoutId = null;
|
||||
}, OPTIMA_DRAWER_HOVER_ENTER_DELAY);
|
||||
},
|
||||
peekDrawerLeave: () => {
|
||||
cancelPeekTimers('enter');
|
||||
|
||||
// only start leave timer if currently peeking
|
||||
const { drawerIsPeeking } = _get();
|
||||
if (!drawerIsPeeking) return;
|
||||
|
||||
// start a new timer to hide the drawer
|
||||
cancelPeekTimers('leave');
|
||||
peekLeaveTimeoutId = setTimeout(() => {
|
||||
_set({ drawerIsPeeking: false });
|
||||
peekLeaveTimeoutId = null;
|
||||
}, OPTIMA_DRAWER_HOVER_TIMEOUT);
|
||||
},
|
||||
peekDrawerEnter: () => drawerPeek.enter(_get, _set),
|
||||
peekDrawerLeave: () => drawerPeek.leave(_get, _set),
|
||||
|
||||
closePanel: () => {
|
||||
// NOTE: would this make sense?
|
||||
// if (Date.now() - _get().lastPanelOpenTime >= 100)
|
||||
// _set({ panelIsOpen: false });
|
||||
_set({ panelIsOpen: false });
|
||||
// prevent accidental immediate close (e.g. double-click, animation protection)
|
||||
if (Date.now() - _get().lastPanelOpenTime < OPTIMA_OPEN_DEBOUNCE) return;
|
||||
panelPeek.cancel();
|
||||
_set({ panelIsOpen: false, panelIsPeeking: false });
|
||||
},
|
||||
openPanel: () => {
|
||||
panelPeek.cancel();
|
||||
_set({ panelIsOpen: true, panelIsPeeking: false, lastPanelOpenTime: Date.now() });
|
||||
},
|
||||
openPanel: () => _set({ panelIsOpen: true, lastPanelOpenTime: Date.now() }),
|
||||
togglePanel: () => _get().panelIsOpen ? _get().closePanel() : _get().openPanel(),
|
||||
peekPanelEnter: () => panelPeek.enter(_get, _set),
|
||||
peekPanelLeave: () => panelPeek.leave(_get, _set),
|
||||
|
||||
closeAIXDebugger: () => _set({ showAIXDebugger: false }),
|
||||
openAIXDebugger: () => _set({ ...modalsClosedState, showAIXDebugger: true }),
|
||||
|
||||
Reference in New Issue
Block a user