feat: scroll nav mode setting (scrollbar, buttons, none)
Adds a Nav selector in settings to choose between pixel art scrollbar, aquatic arrow buttons, or no scroll navigation. Jump % slider only shows in buttons mode. Persisted in localStorage.
This commit is contained in:
@@ -162,6 +162,18 @@ function setInputMaxLines(val: number) {
|
||||
localStorage.setItem('transcript-input-max-lines', String(val))
|
||||
}
|
||||
|
||||
// Scroll nav mode: 'scrollbar' | 'buttons' | 'none'
|
||||
type ScrollNavMode = 'scrollbar' | 'buttons' | 'none'
|
||||
const savedScrollNav = localStorage.getItem('transcript-scroll-nav') as ScrollNavMode | null
|
||||
const scrollNavMode = ref<ScrollNavMode>(
|
||||
savedScrollNav && ['scrollbar', 'buttons', 'none'].includes(savedScrollNav) ? savedScrollNav : 'buttons'
|
||||
)
|
||||
|
||||
function setScrollNavMode(val: ScrollNavMode) {
|
||||
scrollNavMode.value = val
|
||||
localStorage.setItem('transcript-scroll-nav', val)
|
||||
}
|
||||
|
||||
// Scroll jump percent
|
||||
const savedScrollJump = localStorage.getItem('transcript-scroll-jump')
|
||||
const scrollJumpPercent = ref(savedScrollJump !== null ? parseInt(savedScrollJump) : 50)
|
||||
@@ -538,7 +550,10 @@ onBeforeUnmount(() => {
|
||||
resizing: isResizing,
|
||||
mobile: effectiveMobile,
|
||||
'chrome-visible': showChrome,
|
||||
'selector-open': showSelector
|
||||
'selector-open': showSelector,
|
||||
'nav-scrollbar': scrollNavMode === 'scrollbar',
|
||||
'nav-buttons': scrollNavMode === 'buttons',
|
||||
'nav-none': scrollNavMode === 'none'
|
||||
}"
|
||||
:style="windowStyle"
|
||||
@mouseenter="isHovered = true"
|
||||
@@ -703,6 +718,7 @@ onBeforeUnmount(() => {
|
||||
:overlay-opacity="overlayOpacity"
|
||||
:input-max-lines="inputMaxLines"
|
||||
:scroll-jump-percent="scrollJumpPercent"
|
||||
:scroll-nav-mode="scrollNavMode"
|
||||
:hook-permission-mode="hookMeta.permissionMode"
|
||||
@send="handleSend"
|
||||
@switch-agent="handleAgentSwitch"
|
||||
@@ -717,6 +733,7 @@ onBeforeUnmount(() => {
|
||||
@update:overlay-opacity="setOverlayOpacity"
|
||||
@update:input-max-lines="setInputMaxLines"
|
||||
@update:scroll-jump-percent="setScrollJumpPercent"
|
||||
@update:scroll-nav-mode="setScrollNavMode"
|
||||
/>
|
||||
<div v-else class="empty-state">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
@@ -729,7 +746,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- Scroll arrows (visible only in idle mode) -->
|
||||
<Transition name="scroll-arrows">
|
||||
<div v-if="showChrome && conversation" class="scroll-arrows">
|
||||
<div v-if="showChrome && conversation && scrollNavMode === 'buttons'" class="scroll-arrows">
|
||||
<!-- Top: double chevron up + wave accent -->
|
||||
<button class="scroll-arrow sa-surface" @click="scrollToEdge('top')" title="Scroll to top">
|
||||
<svg width="20" height="22" viewBox="0 0 20 22" shape-rendering="crispEdges">
|
||||
@@ -1204,13 +1221,53 @@ onBeforeUnmount(() => {
|
||||
padding-top: 5rem !important;
|
||||
padding-bottom: 5rem !important;
|
||||
flex: 1 !important;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for buttons and none modes */
|
||||
.aero-win:not(.nav-scrollbar) .content :deep(.messages-scroll) {
|
||||
scrollbar-width: none !important;
|
||||
}
|
||||
|
||||
.content :deep(.messages-scroll)::-webkit-scrollbar {
|
||||
.aero-win:not(.nav-scrollbar) .content :deep(.messages-scroll)::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Pixel art scrollbar (only in scrollbar mode) */
|
||||
.aero-win.nav-scrollbar .content :deep(.messages-scroll) {
|
||||
scrollbar-gutter: stable !important;
|
||||
}
|
||||
|
||||
.aero-win.nav-scrollbar .content :deep(.messages-scroll)::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
/* Idle: hide scrollbar visuals (gutter stays) */
|
||||
.aero-win.nav-scrollbar:not(.chrome-visible) .content :deep(.messages-scroll)::-webkit-scrollbar-track {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.aero-win.nav-scrollbar:not(.chrome-visible) .content :deep(.messages-scroll)::-webkit-scrollbar-thumb {
|
||||
background: transparent !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.aero-win.nav-scrollbar .content :deep(.messages-scroll)::-webkit-scrollbar-track {
|
||||
background:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='8' height='8' fill='%230c2d4a' opacity='0.4'/%3E%3Crect x='2' y='2' width='2' height='2' fill='%23075985' opacity='0.15'/%3E%3Crect x='6' y='6' width='2' height='2' fill='%23075985' opacity='0.1'/%3E%3C/svg%3E") repeat;
|
||||
}
|
||||
|
||||
.aero-win.nav-scrollbar .content :deep(.messages-scroll)::-webkit-scrollbar-thumb {
|
||||
background:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='16' viewBox='0 0 8 16' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='8' height='16' fill='%230ea5e9' opacity='0.3'/%3E%3Crect x='2' y='2' width='4' height='2' fill='%2322d3ee' opacity='0.25'/%3E%3Crect x='2' y='6' width='4' height='2' fill='%2367e8f9' opacity='0.2'/%3E%3Crect x='2' y='10' width='4' height='2' fill='%2322d3ee' opacity='0.25'/%3E%3Crect x='2' y='14' width='4' height='2' fill='%2367e8f9' opacity='0.15'/%3E%3C/svg%3E") repeat;
|
||||
border: 1px solid rgba(14, 165, 233, 0.15);
|
||||
}
|
||||
|
||||
.aero-win.nav-scrollbar .content :deep(.messages-scroll)::-webkit-scrollbar-thumb:hover {
|
||||
background:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='16' viewBox='0 0 8 16' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='8' height='16' fill='%230ea5e9' opacity='0.45'/%3E%3Crect x='2' y='2' width='4' height='2' fill='%2322d3ee' opacity='0.35'/%3E%3Crect x='2' y='6' width='4' height='2' fill='%2367e8f9' opacity='0.3'/%3E%3Crect x='2' y='10' width='4' height='2' fill='%2322d3ee' opacity='0.35'/%3E%3Crect x='2' y='14' width='4' height='2' fill='%2367e8f9' opacity='0.25'/%3E%3C/svg%3E") repeat;
|
||||
border-color: rgba(14, 165, 233, 0.3);
|
||||
}
|
||||
|
||||
.content :deep(.meta-badge) {
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: rgba(255,255,255,0.4);
|
||||
|
||||
@@ -40,6 +40,7 @@ const props = defineProps<{
|
||||
overlayOpacity?: number
|
||||
inputMaxLines?: number
|
||||
scrollJumpPercent?: number
|
||||
scrollNavMode?: 'scrollbar' | 'buttons' | 'none'
|
||||
hookPermissionMode?: string
|
||||
}>()
|
||||
|
||||
@@ -67,6 +68,7 @@ const emit = defineEmits<{
|
||||
'update:overlayOpacity': [value: number]
|
||||
'update:inputMaxLines': [value: number]
|
||||
'update:scrollJumpPercent': [value: number]
|
||||
'update:scrollNavMode': [value: 'scrollbar' | 'buttons' | 'none']
|
||||
}>()
|
||||
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
@@ -507,7 +509,18 @@ function formatDuration(start: string, end: string): string {
|
||||
<span class="overlay-value">{{ inputMaxLines ?? 6 }}</span>
|
||||
</div>
|
||||
<div class="selector-row">
|
||||
<label class="selector-label">Scroll</label>
|
||||
<label class="selector-label">Nav</label>
|
||||
<div class="agent-selector">
|
||||
<button
|
||||
v-for="mode in (['scrollbar', 'buttons', 'none'] as const)"
|
||||
:key="mode"
|
||||
:class="['agent-btn', { active: (scrollNavMode ?? 'buttons') === mode }]"
|
||||
@click="emit('update:scrollNavMode', mode)"
|
||||
>{{ mode }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="(scrollNavMode ?? 'buttons') === 'buttons'" class="selector-row">
|
||||
<label class="selector-label">Jump</label>
|
||||
<input
|
||||
type="range"
|
||||
class="overlay-slider"
|
||||
|
||||
Reference in New Issue
Block a user