mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-16 08:10:40 +02:00
fix macos issues, windows issues, themeing issues
This commit is contained in:
@@ -35,10 +35,12 @@ let mainWindowBoundsTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let mainRendererReady = false
|
||||
let allowMainWindowClose = false
|
||||
let mainWindowClosePending = false
|
||||
let suppressNextMainWindowBoundsEvent = false
|
||||
|
||||
const scopePopoutWindows = new Map<ScopeKind, BrowserWindow>()
|
||||
const scopePopoutCloseAllowed = new Set<ScopeKind>()
|
||||
const popoutBoundsTimers = new Map<ScopeKind, ReturnType<typeof setTimeout>>()
|
||||
const suppressNextPopoutBoundsEvents = new Set<ScopeKind>()
|
||||
const windowSettingsHeights = new Map<number, number>()
|
||||
const windowSettingsBottomAnchors = new Map<number, number>()
|
||||
const pendingProfileOpenPaths: string[] = []
|
||||
@@ -189,6 +191,10 @@ function scheduleMainWindowBoundsSave(window: BrowserWindow): void {
|
||||
mainWindowBoundsTimer = setTimeout(() => {
|
||||
mainWindowBoundsTimer = null
|
||||
if (window.isDestroyed() || window.webContents.isDestroyed()) return
|
||||
if (suppressNextMainWindowBoundsEvent) {
|
||||
suppressNextMainWindowBoundsEvent = false
|
||||
return
|
||||
}
|
||||
window.webContents.send('window:bounds-changed', toLogicalBounds(window))
|
||||
}, 80)
|
||||
}
|
||||
@@ -565,6 +571,10 @@ function emitPopoutBoundsChanged(kind: ScopeKind, window: BrowserWindow): void {
|
||||
popoutBoundsTimers.delete(kind)
|
||||
|
||||
if (!mainWindow || mainWindow.isDestroyed() || window.isDestroyed()) return
|
||||
if (suppressNextPopoutBoundsEvents.has(kind)) {
|
||||
suppressNextPopoutBoundsEvents.delete(kind)
|
||||
return
|
||||
}
|
||||
const bounds = toLogicalBounds(window)
|
||||
mainWindow.webContents.send('scope-popout:bounds-changed', kind, bounds)
|
||||
}, 80)
|
||||
@@ -582,11 +592,13 @@ function destroyScopePopoutWindow(kind: ScopeKind): void {
|
||||
popoutBoundsTimers.delete(kind)
|
||||
}
|
||||
|
||||
suppressNextPopoutBoundsEvents.delete(kind)
|
||||
scopePopoutCloseAllowed.add(kind)
|
||||
scopePopoutWindows.delete(kind)
|
||||
|
||||
if (!window.isDestroyed()) {
|
||||
window.close()
|
||||
return
|
||||
}
|
||||
|
||||
scopePopoutCloseAllowed.delete(kind)
|
||||
@@ -608,6 +620,7 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro
|
||||
height: POPOUT_DEFAULTS.height,
|
||||
}
|
||||
const bounds = normalizeBounds(rawBounds, fallbackBounds)
|
||||
suppressNextPopoutBoundsEvents.add(kind)
|
||||
|
||||
const options: BrowserWindowConstructorOptions = {
|
||||
x: bounds.x,
|
||||
@@ -697,6 +710,7 @@ function syncScopePopouts(nextState: ScopePopoutSyncStateMap): void {
|
||||
|| currentBounds.height !== nextBounds.height
|
||||
|
||||
if (hasBoundsDelta) {
|
||||
suppressNextPopoutBoundsEvents.add(kind)
|
||||
applyLogicalBounds(popoutWindow, nextBounds)
|
||||
}
|
||||
}
|
||||
@@ -1044,6 +1058,9 @@ function setupIPC(): void {
|
||||
const targetWindow = getWindowFromSender(event.sender)
|
||||
if (!targetWindow) return
|
||||
|
||||
if (isMainRendererWindow(targetWindow)) {
|
||||
suppressNextMainWindowBoundsEvent = true
|
||||
}
|
||||
applyLogicalBounds(targetWindow, bounds)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useLayoutEffect, useRef, type CSSProperties, type JSX } from 'react'
|
||||
import { useEffect, useLayoutEffect, useRef, type CSSProperties, type JSX, type WheelEvent } from 'react'
|
||||
import { useAudioStore } from '../stores/audioStore'
|
||||
import { usePerformanceStore } from '../stores/performanceStore'
|
||||
import { useSettingsStore } from '../stores/settingsStore'
|
||||
import { useThemeStore } from '../stores/themeStore'
|
||||
import { getHorizontalWheelScrollResult } from '../utils/horizontalWheelScroll'
|
||||
import type { ScopeKind } from '../../types/scope'
|
||||
import { VISUALIZER_FRAME_TARGETS, type VisualizerFrameTarget } from '../../types/performance'
|
||||
import { SCOPE_KINDS } from '../../types/scope'
|
||||
@@ -167,9 +168,31 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
|
||||
setThemeId(useThemeStore.getState().activeThemeId)
|
||||
}
|
||||
|
||||
const handleRailWheel = (event: WheelEvent<HTMLDivElement>): void => {
|
||||
const railElement = event.currentTarget
|
||||
const target = event.target
|
||||
const isTargetExcluded = target instanceof Element
|
||||
&& target.closest('input[type="range"], select, .settings-control__select') !== null
|
||||
|
||||
const scrollResult = getHorizontalWheelScrollResult({
|
||||
clientWidth: railElement.clientWidth,
|
||||
deltaMode: event.deltaMode,
|
||||
deltaX: event.deltaX,
|
||||
deltaY: event.deltaY,
|
||||
isTargetExcluded,
|
||||
scrollLeft: railElement.scrollLeft,
|
||||
scrollWidth: railElement.scrollWidth,
|
||||
})
|
||||
|
||||
if (!scrollResult) return
|
||||
|
||||
railElement.scrollLeft = scrollResult.nextScrollLeft
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bottom-bar" ref={rootRef}>
|
||||
<div className="bottom-bar__rail" aria-label="Global settings">
|
||||
<div className="bottom-bar__rail" aria-label="Global settings" onWheel={handleRailWheel}>
|
||||
<div className="bottom-bar__rail-content">
|
||||
<section className="bottom-bar__section bottom-bar__section--modules">
|
||||
<div className="bottom-bar__section-title">Modules</div>
|
||||
|
||||
@@ -21,11 +21,10 @@ export interface ProfileDraftSource {
|
||||
export function buildProfileDraft(
|
||||
source: ProfileDraftSource,
|
||||
name: string,
|
||||
fallbackThemeId: string | null = null,
|
||||
): Profile {
|
||||
return normalizeProfile({
|
||||
name,
|
||||
themeId: source.themeId ?? fallbackThemeId,
|
||||
themeId: source.themeId,
|
||||
scopeOrder: [...source.scopeOrder],
|
||||
hiddenScopes: Array.from(source.hiddenScopes),
|
||||
widthWeights: { ...source.widthWeights },
|
||||
|
||||
@@ -26,6 +26,7 @@ export type { ScopeSettings } from '../../types/settings'
|
||||
const STORAGE_KEY = 'prism:settings'
|
||||
const PROFILES_STORAGE_KEY = 'prism:profiles'
|
||||
const ACTIVE_PROFILE_KEY = 'prism:activeProfile'
|
||||
const PROFILE_GEOMETRY_SYNC_WINDOW_MS = 800
|
||||
|
||||
interface PersistedSettingsState {
|
||||
themeId: string | null
|
||||
@@ -53,6 +54,7 @@ interface SettingsState extends WorkingSettingsState {
|
||||
activeProfileId: string | null
|
||||
savedProfileBaseline: Profile | null
|
||||
hasUnsavedProfileChanges: boolean
|
||||
geometrySyncUntil: number
|
||||
initializeProfiles: () => Promise<void>
|
||||
applyExternalProfileSnapshot: (snapshot: ProfileLibrarySnapshot) => void
|
||||
setThemeId: (themeId: string | null) => void
|
||||
@@ -190,11 +192,7 @@ function buildActiveProfileDraft(state: SettingsState): Profile | null {
|
||||
return null
|
||||
}
|
||||
|
||||
return buildProfileDraft(
|
||||
state,
|
||||
activeProfileName,
|
||||
useThemeStore.getState().activeThemeId,
|
||||
)
|
||||
return buildProfileDraft(state, activeProfileName)
|
||||
}
|
||||
|
||||
export function hasProfileDraftChanges(state: SettingsState, baseline = state.savedProfileBaseline): boolean {
|
||||
@@ -232,16 +230,32 @@ function commitWorkingState(
|
||||
return nextState
|
||||
}
|
||||
|
||||
function nextGeometrySyncDeadline(): number {
|
||||
return Date.now() + PROFILE_GEOMETRY_SYNC_WINDOW_MS
|
||||
}
|
||||
|
||||
function isWithinGeometrySyncWindow(state: Pick<SettingsState, 'geometrySyncUntil'>): boolean {
|
||||
return state.geometrySyncUntil > Date.now()
|
||||
}
|
||||
|
||||
function syncMissingBaselineWindowBounds(state: SettingsState, bounds: WindowBounds): Profile | null {
|
||||
const baseline = state.savedProfileBaseline
|
||||
if (!baseline || state.hasUnsavedProfileChanges || baseline.windowBounds) {
|
||||
if (!baseline || state.hasUnsavedProfileChanges) {
|
||||
return baseline
|
||||
}
|
||||
|
||||
return normalizeProfile({
|
||||
...baseline,
|
||||
windowBounds: bounds,
|
||||
}, baseline.name)
|
||||
if (isWithinGeometrySyncWindow(state)) {
|
||||
return normalizeProfile({
|
||||
...baseline,
|
||||
windowBounds: bounds,
|
||||
}, baseline.name)
|
||||
}
|
||||
|
||||
if (baseline.windowBounds) {
|
||||
return baseline
|
||||
}
|
||||
|
||||
return baseline
|
||||
}
|
||||
|
||||
function syncMissingBaselinePopoutBounds(
|
||||
@@ -251,20 +265,28 @@ function syncMissingBaselinePopoutBounds(
|
||||
): Profile | null {
|
||||
const baseline = state.savedProfileBaseline
|
||||
const baselinePopout = baseline?.scopePopouts[kind]
|
||||
if (!baseline || state.hasUnsavedProfileChanges || !baselinePopout?.poppedOut || baselinePopout.windowBounds) {
|
||||
if (!baseline || state.hasUnsavedProfileChanges || !baselinePopout?.poppedOut) {
|
||||
return baseline
|
||||
}
|
||||
|
||||
return normalizeProfile({
|
||||
...baseline,
|
||||
scopePopouts: {
|
||||
...baseline.scopePopouts,
|
||||
[kind]: {
|
||||
...baselinePopout,
|
||||
windowBounds: bounds,
|
||||
if (isWithinGeometrySyncWindow(state)) {
|
||||
return normalizeProfile({
|
||||
...baseline,
|
||||
scopePopouts: {
|
||||
...baseline.scopePopouts,
|
||||
[kind]: {
|
||||
...baselinePopout,
|
||||
windowBounds: bounds,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, baseline.name)
|
||||
}, baseline.name)
|
||||
}
|
||||
|
||||
if (baselinePopout.windowBounds) {
|
||||
return baseline
|
||||
}
|
||||
|
||||
return baseline
|
||||
}
|
||||
|
||||
function applyLoadedProfileEffects(profile: Profile | null): void {
|
||||
@@ -282,6 +304,39 @@ function applyLoadedProfileEffects(profile: Profile | null): void {
|
||||
}
|
||||
}
|
||||
|
||||
function syncCurrentMainWindowBounds(
|
||||
set: (updater: (state: SettingsState) => SettingsState) => void,
|
||||
): void {
|
||||
if (!canUseElectronAPI()) {
|
||||
return
|
||||
}
|
||||
|
||||
void window.electronAPI.getWindowBounds().then((bounds) => {
|
||||
if (!bounds) {
|
||||
return
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const baseline = state.savedProfileBaseline
|
||||
if (!baseline || state.hasUnsavedProfileChanges) {
|
||||
return state
|
||||
}
|
||||
|
||||
const nextState = commitWorkingState(state, {
|
||||
windowBounds: bounds,
|
||||
}, normalizeProfile({
|
||||
...baseline,
|
||||
windowBounds: bounds,
|
||||
}, baseline.name))
|
||||
|
||||
return {
|
||||
...nextState,
|
||||
geometrySyncUntil: 0,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function applyProfileSnapshot(
|
||||
set: (updater: (state: SettingsState) => SettingsState) => void,
|
||||
snapshot: ProfileLibrarySnapshot,
|
||||
@@ -300,6 +355,7 @@ function applyProfileSnapshot(
|
||||
...state,
|
||||
profiles: snapshot.profiles,
|
||||
activeProfileId: snapshot.activeProfileId,
|
||||
geometrySyncUntil: 0,
|
||||
}, baselineProfile)
|
||||
}
|
||||
|
||||
@@ -309,6 +365,7 @@ function applyProfileSnapshot(
|
||||
...nextWorkingState,
|
||||
profiles: snapshot.profiles,
|
||||
activeProfileId: snapshot.activeProfileId,
|
||||
geometrySyncUntil: nextGeometrySyncDeadline(),
|
||||
}, baselineProfile)
|
||||
|
||||
persistWorkingState(nextState)
|
||||
@@ -317,6 +374,7 @@ function applyProfileSnapshot(
|
||||
|
||||
if (options.loadActiveProfile) {
|
||||
applyLoadedProfileEffects(activeProfile)
|
||||
syncCurrentMainWindowBounds(set)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,11 +444,14 @@ async function restoreSavedProfileBaseline(
|
||||
const nextState = withProfileDraftState({
|
||||
...state,
|
||||
...nextWorkingState,
|
||||
geometrySyncUntil: nextGeometrySyncDeadline(),
|
||||
}, baseline)
|
||||
|
||||
persistWorkingState(nextState)
|
||||
return nextState
|
||||
})
|
||||
|
||||
syncCurrentMainWindowBounds(set)
|
||||
}
|
||||
|
||||
const stored = canUseElectronAPI() ? {} : loadFromStorage()
|
||||
@@ -414,6 +475,7 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
activeProfileId: null,
|
||||
savedProfileBaseline: null,
|
||||
hasUnsavedProfileChanges: false,
|
||||
geometrySyncUntil: 0,
|
||||
|
||||
visibleScopes: () => {
|
||||
const { scopeOrder, hiddenScopes } = get()
|
||||
@@ -528,8 +590,9 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
|
||||
updatePopoutBounds: (kind: ScopeKind, bounds: WindowBounds) => {
|
||||
set((state) => {
|
||||
const isSyncingGeometry = isWithinGeometrySyncWindow(state)
|
||||
const nextBaseline = syncMissingBaselinePopoutBounds(state, kind, bounds)
|
||||
return commitWorkingState(state, {
|
||||
const nextState = commitWorkingState(state, {
|
||||
scopePopouts: {
|
||||
...state.scopePopouts,
|
||||
[kind]: {
|
||||
@@ -538,13 +601,26 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
},
|
||||
},
|
||||
}, nextBaseline)
|
||||
return isSyncingGeometry
|
||||
? {
|
||||
...nextState,
|
||||
geometrySyncUntil: nextGeometrySyncDeadline(),
|
||||
}
|
||||
: nextState
|
||||
})
|
||||
},
|
||||
|
||||
updateMainWindowBounds: (bounds: WindowBounds) => {
|
||||
set((state) => {
|
||||
const isSyncingGeometry = isWithinGeometrySyncWindow(state)
|
||||
const nextBaseline = syncMissingBaselineWindowBounds(state, bounds)
|
||||
return commitWorkingState(state, { windowBounds: bounds }, nextBaseline)
|
||||
const nextState = commitWorkingState(state, { windowBounds: bounds }, nextBaseline)
|
||||
return isSyncingGeometry
|
||||
? {
|
||||
...nextState,
|
||||
geometrySyncUntil: nextGeometrySyncDeadline(),
|
||||
}
|
||||
: nextState
|
||||
})
|
||||
},
|
||||
|
||||
@@ -584,7 +660,7 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
|
||||
const snapshot = await window.electronAPI.saveNewProfile(
|
||||
name,
|
||||
buildProfileDraft(get(), name, useThemeStore.getState().activeThemeId),
|
||||
buildProfileDraft(get(), name),
|
||||
)
|
||||
applyProfileSnapshot(set, snapshot, { loadActiveProfile: false })
|
||||
return snapshot.activeProfileId
|
||||
@@ -604,7 +680,7 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
|
||||
const snapshot = await window.electronAPI.overwriteProfile(
|
||||
id,
|
||||
buildProfileDraft(state, name, useThemeStore.getState().activeThemeId),
|
||||
buildProfileDraft(state, name),
|
||||
)
|
||||
applyProfileSnapshot(set, snapshot, { loadActiveProfile: false })
|
||||
},
|
||||
|
||||
@@ -191,7 +191,7 @@ select {
|
||||
background:
|
||||
linear-gradient(180deg, var(--settings-bg-top), var(--settings-bg-bottom)),
|
||||
rgba(0, 0, 0, 0.96);
|
||||
border-top: 1px solid var(--divider);
|
||||
border-top: 1px solid rgba(var(--accent-rgb), 0.14);
|
||||
box-shadow: 0 -16px 34px rgba(0, 0, 0, 0.34);
|
||||
}
|
||||
|
||||
@@ -239,8 +239,9 @@ select {
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--control-border);
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.22);
|
||||
background: linear-gradient(180deg, rgba(var(--accent-rgb), 0.12), rgba(255, 255, 255, 0.02));
|
||||
box-shadow: inset 0 1px 0 rgba(var(--accent-rgb), 0.08);
|
||||
}
|
||||
|
||||
.toolbar__brand-mark {
|
||||
@@ -339,7 +340,7 @@ select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--menu-bg);
|
||||
border: 1px solid var(--menu-border);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.22);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
z-index: 20;
|
||||
@@ -629,7 +630,7 @@ select {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 0 16px 14px;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-left: 1px solid rgba(var(--accent-rgb), 0.12);
|
||||
}
|
||||
|
||||
.settings-scope-section:first-child {
|
||||
@@ -643,7 +644,7 @@ select {
|
||||
gap: 12px;
|
||||
padding: 10px 0 12px;
|
||||
min-height: 42px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-bottom: 1px solid rgba(var(--accent-rgb), 0.14);
|
||||
}
|
||||
|
||||
.settings-scope-section__title {
|
||||
@@ -713,9 +714,9 @@ select {
|
||||
padding: 0 12px;
|
||||
padding-right: 36px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid transparent;
|
||||
background: linear-gradient(180deg, var(--input-bg), var(--settings-bg-bottom));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
background: linear-gradient(180deg, rgba(var(--accent-rgb), 0.1), rgba(7, 10, 15, 0.96));
|
||||
box-shadow: inset 0 1px 0 rgba(var(--accent-rgb), 0.08);
|
||||
color: var(--text-primary);
|
||||
font-size: 11px;
|
||||
transition: border-color 140ms ease, background-color 140ms ease, box-shadow 140ms ease, color 140ms ease;
|
||||
@@ -723,8 +724,8 @@ select {
|
||||
|
||||
.settings-control__select:hover,
|
||||
.settings-control__select:focus-within {
|
||||
border-color: var(--input-border-focus);
|
||||
background: linear-gradient(180deg, var(--input-bg-focus), var(--settings-bg-bottom));
|
||||
border-color: rgba(var(--accent-rgb), 0.34);
|
||||
background: linear-gradient(180deg, rgba(var(--accent-rgb), 0.16), rgba(8, 12, 18, 0.98));
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -918,8 +919,9 @@ select {
|
||||
min-height: 32px;
|
||||
padding: 0 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--control-border);
|
||||
background: linear-gradient(180deg, var(--input-bg), var(--settings-bg-bottom));
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.22);
|
||||
background: linear-gradient(180deg, rgba(var(--accent-rgb), 0.12), rgba(7, 10, 15, 0.96));
|
||||
box-shadow: inset 0 1px 0 rgba(var(--accent-rgb), 0.08);
|
||||
color: var(--text-secondary);
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
@@ -937,16 +939,31 @@ select {
|
||||
box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.3);
|
||||
}
|
||||
|
||||
.settings-status-pill.is-connecting {
|
||||
border-color: rgba(var(--accent-rgb), 0.32);
|
||||
background: linear-gradient(180deg, rgba(var(--accent-rgb), 0.16), rgba(7, 10, 15, 0.96));
|
||||
}
|
||||
|
||||
.settings-status-pill.is-capturing .settings-status-pill__dot {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 8px rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.settings-status-pill.is-capturing {
|
||||
border-color: rgba(34, 197, 94, 0.34);
|
||||
background: linear-gradient(180deg, rgba(34, 197, 94, 0.14), rgba(7, 10, 15, 0.96));
|
||||
}
|
||||
|
||||
.settings-status-pill.is-error .settings-status-pill__dot {
|
||||
background: var(--danger);
|
||||
box-shadow: 0 0 8px rgba(248, 113, 113, 0.36);
|
||||
}
|
||||
|
||||
.settings-status-pill.is-error {
|
||||
border-color: rgba(248, 113, 113, 0.34);
|
||||
background: linear-gradient(180deg, rgba(248, 113, 113, 0.14), rgba(7, 10, 15, 0.96));
|
||||
}
|
||||
|
||||
.settings-error-text {
|
||||
margin-top: 8px;
|
||||
color: var(--danger);
|
||||
@@ -1034,7 +1051,7 @@ select {
|
||||
min-height: 92px;
|
||||
padding: 0;
|
||||
background: var(--bottom-bar-bg);
|
||||
border-top: 1px solid var(--divider);
|
||||
border-top: 1px solid rgba(var(--accent-rgb), 0.14);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1112,8 +1129,9 @@ select {
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--control-border);
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.22);
|
||||
background: linear-gradient(180deg, rgba(var(--accent-rgb), 0.08), rgba(255, 255, 255, 0.02));
|
||||
box-shadow: inset 0 1px 0 rgba(var(--accent-rgb), 0.08);
|
||||
}
|
||||
|
||||
.bottom-bar__inline--theme {
|
||||
@@ -1128,7 +1146,7 @@ select {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
margin: 0 16px 0 0;
|
||||
background: linear-gradient(180deg, transparent, var(--divider) 18%, rgba(255, 255, 255, 0.04) 82%, transparent);
|
||||
background: linear-gradient(180deg, transparent, rgba(var(--accent-rgb), 0.18) 18%, rgba(var(--accent-rgb), 0.08) 82%, transparent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1175,7 +1193,7 @@ select {
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 10px 14px 10px 16px;
|
||||
border-left: 1px solid var(--divider);
|
||||
border-left: 1px solid rgba(var(--accent-rgb), 0.14);
|
||||
background: linear-gradient(90deg, rgba(2, 4, 7, 0.76), var(--bottom-bar-bg) 24%, var(--bottom-bar-bg));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
const WHEEL_DELTA_LINE_PX = 16
|
||||
const WHEEL_DELTA_PAGE_WIDTH_FACTOR = 0.9
|
||||
|
||||
export interface HorizontalWheelScrollInput {
|
||||
clientWidth: number
|
||||
deltaMode: number
|
||||
deltaX: number
|
||||
deltaY: number
|
||||
isTargetExcluded?: boolean
|
||||
scrollLeft: number
|
||||
scrollWidth: number
|
||||
}
|
||||
|
||||
export interface HorizontalWheelScrollResult {
|
||||
appliedDelta: number
|
||||
nextScrollLeft: number
|
||||
}
|
||||
|
||||
export function normalizeWheelDelta(delta: number, deltaMode: number, clientWidth: number): number {
|
||||
if (deltaMode === 1) {
|
||||
return delta * WHEEL_DELTA_LINE_PX
|
||||
}
|
||||
|
||||
if (deltaMode === 2) {
|
||||
return delta * clientWidth * WHEEL_DELTA_PAGE_WIDTH_FACTOR
|
||||
}
|
||||
|
||||
return delta
|
||||
}
|
||||
|
||||
export function getHorizontalWheelScrollResult({
|
||||
clientWidth,
|
||||
deltaMode,
|
||||
deltaX,
|
||||
deltaY,
|
||||
isTargetExcluded = false,
|
||||
scrollLeft,
|
||||
scrollWidth,
|
||||
}: HorizontalWheelScrollInput): HorizontalWheelScrollResult | null {
|
||||
const maxScrollLeft = Math.max(0, scrollWidth - clientWidth)
|
||||
if (isTargetExcluded || maxScrollLeft <= 0) return null
|
||||
if (deltaX !== 0 || deltaY === 0) return null
|
||||
|
||||
const normalizedDelta = normalizeWheelDelta(deltaY, deltaMode, clientWidth)
|
||||
if (normalizedDelta === 0) return null
|
||||
|
||||
const nextScrollLeft = Math.min(maxScrollLeft, Math.max(0, scrollLeft + normalizedDelta))
|
||||
if (nextScrollLeft === scrollLeft) return null
|
||||
|
||||
return {
|
||||
appliedDelta: nextScrollLeft - scrollLeft,
|
||||
nextScrollLeft,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user