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))
|
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
|
// Scroll jump percent
|
||||||
const savedScrollJump = localStorage.getItem('transcript-scroll-jump')
|
const savedScrollJump = localStorage.getItem('transcript-scroll-jump')
|
||||||
const scrollJumpPercent = ref(savedScrollJump !== null ? parseInt(savedScrollJump) : 50)
|
const scrollJumpPercent = ref(savedScrollJump !== null ? parseInt(savedScrollJump) : 50)
|
||||||
@@ -538,7 +550,10 @@ onBeforeUnmount(() => {
|
|||||||
resizing: isResizing,
|
resizing: isResizing,
|
||||||
mobile: effectiveMobile,
|
mobile: effectiveMobile,
|
||||||
'chrome-visible': showChrome,
|
'chrome-visible': showChrome,
|
||||||
'selector-open': showSelector
|
'selector-open': showSelector,
|
||||||
|
'nav-scrollbar': scrollNavMode === 'scrollbar',
|
||||||
|
'nav-buttons': scrollNavMode === 'buttons',
|
||||||
|
'nav-none': scrollNavMode === 'none'
|
||||||
}"
|
}"
|
||||||
:style="windowStyle"
|
:style="windowStyle"
|
||||||
@mouseenter="isHovered = true"
|
@mouseenter="isHovered = true"
|
||||||
@@ -703,6 +718,7 @@ onBeforeUnmount(() => {
|
|||||||
:overlay-opacity="overlayOpacity"
|
:overlay-opacity="overlayOpacity"
|
||||||
:input-max-lines="inputMaxLines"
|
:input-max-lines="inputMaxLines"
|
||||||
:scroll-jump-percent="scrollJumpPercent"
|
:scroll-jump-percent="scrollJumpPercent"
|
||||||
|
:scroll-nav-mode="scrollNavMode"
|
||||||
:hook-permission-mode="hookMeta.permissionMode"
|
:hook-permission-mode="hookMeta.permissionMode"
|
||||||
@send="handleSend"
|
@send="handleSend"
|
||||||
@switch-agent="handleAgentSwitch"
|
@switch-agent="handleAgentSwitch"
|
||||||
@@ -717,6 +733,7 @@ onBeforeUnmount(() => {
|
|||||||
@update:overlay-opacity="setOverlayOpacity"
|
@update:overlay-opacity="setOverlayOpacity"
|
||||||
@update:input-max-lines="setInputMaxLines"
|
@update:input-max-lines="setInputMaxLines"
|
||||||
@update:scroll-jump-percent="setScrollJumpPercent"
|
@update:scroll-jump-percent="setScrollJumpPercent"
|
||||||
|
@update:scroll-nav-mode="setScrollNavMode"
|
||||||
/>
|
/>
|
||||||
<div v-else class="empty-state">
|
<div v-else class="empty-state">
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<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) -->
|
<!-- Scroll arrows (visible only in idle mode) -->
|
||||||
<Transition name="scroll-arrows">
|
<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 -->
|
<!-- Top: double chevron up + wave accent -->
|
||||||
<button class="scroll-arrow sa-surface" @click="scrollToEdge('top')" title="Scroll to top">
|
<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">
|
<svg width="20" height="22" viewBox="0 0 20 22" shape-rendering="crispEdges">
|
||||||
@@ -1204,13 +1221,53 @@ onBeforeUnmount(() => {
|
|||||||
padding-top: 5rem !important;
|
padding-top: 5rem !important;
|
||||||
padding-bottom: 5rem !important;
|
padding-bottom: 5rem !important;
|
||||||
flex: 1 !important;
|
flex: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for buttons and none modes */
|
||||||
|
.aero-win:not(.nav-scrollbar) .content :deep(.messages-scroll) {
|
||||||
scrollbar-width: none !important;
|
scrollbar-width: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content :deep(.messages-scroll)::-webkit-scrollbar {
|
.aero-win:not(.nav-scrollbar) .content :deep(.messages-scroll)::-webkit-scrollbar {
|
||||||
display: none !important;
|
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) {
|
.content :deep(.meta-badge) {
|
||||||
background: rgba(255,255,255,0.04);
|
background: rgba(255,255,255,0.04);
|
||||||
color: rgba(255,255,255,0.4);
|
color: rgba(255,255,255,0.4);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const props = defineProps<{
|
|||||||
overlayOpacity?: number
|
overlayOpacity?: number
|
||||||
inputMaxLines?: number
|
inputMaxLines?: number
|
||||||
scrollJumpPercent?: number
|
scrollJumpPercent?: number
|
||||||
|
scrollNavMode?: 'scrollbar' | 'buttons' | 'none'
|
||||||
hookPermissionMode?: string
|
hookPermissionMode?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ const emit = defineEmits<{
|
|||||||
'update:overlayOpacity': [value: number]
|
'update:overlayOpacity': [value: number]
|
||||||
'update:inputMaxLines': [value: number]
|
'update:inputMaxLines': [value: number]
|
||||||
'update:scrollJumpPercent': [value: number]
|
'update:scrollJumpPercent': [value: number]
|
||||||
|
'update:scrollNavMode': [value: 'scrollbar' | 'buttons' | 'none']
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const scrollContainer = ref<HTMLElement | null>(null)
|
const scrollContainer = ref<HTMLElement | null>(null)
|
||||||
@@ -507,7 +509,18 @@ function formatDuration(start: string, end: string): string {
|
|||||||
<span class="overlay-value">{{ inputMaxLines ?? 6 }}</span>
|
<span class="overlay-value">{{ inputMaxLines ?? 6 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="selector-row">
|
<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
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
class="overlay-slider"
|
class="overlay-slider"
|
||||||
|
|||||||
Reference in New Issue
Block a user