feat: 3-state size toggle, readability overlay, and input persistence
- Add pin/medium/large size mode button in titlebar with pixel art icons - Dark overlay appears on hover for better text readability - User-input stays visible when textarea has text (CSS :has selector) - Animated size transitions between modes
This commit is contained in:
@@ -72,6 +72,17 @@ const dragOffset = ref({ x: 0, y: 0 })
|
||||
const isResizing = ref(false)
|
||||
const size = ref({ w: 480, h: 600 })
|
||||
|
||||
// Size mode: pin (small, anchored to FAB), medium (default), large
|
||||
type SizeMode = 'pin' | 'medium' | 'large'
|
||||
const sizeMode = ref<SizeMode>('medium')
|
||||
|
||||
function cycleSizeMode() {
|
||||
const modes: SizeMode[] = ['pin', 'medium', 'large']
|
||||
const i = modes.indexOf(sizeMode.value)
|
||||
sizeMode.value = modes[(i + 1) % modes.length]
|
||||
hasCustomPosition.value = false
|
||||
}
|
||||
|
||||
// Mobile bottom sheet state
|
||||
const isMobile = ref(false)
|
||||
const sheetHeight = ref(55)
|
||||
@@ -217,6 +228,12 @@ function stopResize() {
|
||||
// COMPUTED STYLE
|
||||
// ============================================================================
|
||||
|
||||
const sizeModePresets: Record<SizeMode, { w: number; h: number }> = {
|
||||
pin: { w: 240, h: 300 },
|
||||
medium: { w: 480, h: 600 },
|
||||
large: { w: 800, h: 760 }
|
||||
}
|
||||
|
||||
const windowStyle = computed((): Record<string, string> => {
|
||||
if (isMobile.value) {
|
||||
return {
|
||||
@@ -230,22 +247,49 @@ const windowStyle = computed((): Record<string, string> => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCustomPosition.value) {
|
||||
const preset = sizeModePresets[sizeMode.value]
|
||||
|
||||
// Custom position from dragging (uses current resize size or preset)
|
||||
if (hasCustomPosition.value) {
|
||||
const w = sizeMode.value === 'medium' ? size.value.w : preset.w
|
||||
const h = sizeMode.value === 'medium' ? size.value.h : preset.h
|
||||
return {
|
||||
width: `${w}px`,
|
||||
height: `${h}px`,
|
||||
top: `${position.value.y}px`,
|
||||
left: `${position.value.x}px`,
|
||||
bottom: 'auto',
|
||||
right: 'auto'
|
||||
}
|
||||
}
|
||||
|
||||
// Pin: anchored to FAB button (bottom-left corner aligned)
|
||||
if (sizeMode.value === 'pin') {
|
||||
return {
|
||||
width: `${preset.w}px`,
|
||||
height: `${preset.h}px`,
|
||||
bottom: '20px',
|
||||
left: '80px'
|
||||
}
|
||||
}
|
||||
|
||||
// Large: centered-ish, generous size
|
||||
if (sizeMode.value === 'large') {
|
||||
return {
|
||||
width: `${preset.w}px`,
|
||||
height: `${preset.h}px`,
|
||||
bottom: '16px',
|
||||
left: '90px'
|
||||
}
|
||||
}
|
||||
|
||||
// Medium (default)
|
||||
return {
|
||||
width: `${size.value.w}px`,
|
||||
height: `${size.value.h}px`,
|
||||
bottom: '16px',
|
||||
left: '90px'
|
||||
}
|
||||
}
|
||||
return {
|
||||
width: `${size.value.w}px`,
|
||||
height: `${size.value.h}px`,
|
||||
top: `${position.value.y}px`,
|
||||
left: `${position.value.x}px`,
|
||||
bottom: 'auto',
|
||||
right: 'auto'
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
@@ -352,6 +396,22 @@ onBeforeUnmount(() => {
|
||||
<span v-if="selectedAgent" class="agent-badge">{{ selectedAgent }}</span>
|
||||
</div>
|
||||
<div class="window-controls">
|
||||
<button @click.stop="cycleSizeMode" class="size-btn" :title="`Size: ${sizeMode}`">
|
||||
<!-- Pin icon: small square bottom-left -->
|
||||
<svg v-if="sizeMode === 'pin'" width="10" height="10" viewBox="0 0 10 10" shape-rendering="crispEdges">
|
||||
<rect x="0" y="6" width="4" height="4" fill="currentColor"/>
|
||||
</svg>
|
||||
<!-- Medium icon: medium square centered -->
|
||||
<svg v-else-if="sizeMode === 'medium'" width="10" height="10" viewBox="0 0 10 10" shape-rendering="crispEdges">
|
||||
<rect x="2" y="2" width="6" height="6" fill="currentColor" opacity="0.35"/>
|
||||
<rect x="3" y="3" width="4" height="4" fill="currentColor"/>
|
||||
</svg>
|
||||
<!-- Large icon: full square -->
|
||||
<svg v-else width="10" height="10" viewBox="0 0 10 10" shape-rendering="crispEdges">
|
||||
<rect x="0" y="0" width="10" height="10" fill="currentColor" opacity="0.35"/>
|
||||
<rect x="1" y="1" width="8" height="8" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button @click.stop="showSelector = !showSelector" :class="{ active: showSelector }" title="Agent/Session">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
@@ -409,6 +469,7 @@ onBeforeUnmount(() => {
|
||||
<!-- Content -->
|
||||
<div class="content">
|
||||
<BackgroundPixelArt />
|
||||
<div class="readability-overlay" />
|
||||
<ChatContainer
|
||||
v-if="conversation"
|
||||
:conversation="conversation"
|
||||
@@ -435,9 +496,15 @@ onBeforeUnmount(() => {
|
||||
<style scoped>
|
||||
.aero-win {
|
||||
position: fixed;
|
||||
min-width: 360px;
|
||||
min-height: 300px;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
z-index: 9999;
|
||||
transition: width 0.3s ease, height 0.3s ease, top 0.3s ease, left 0.3s ease, bottom 0.3s ease;
|
||||
}
|
||||
|
||||
.aero-win.dragging,
|
||||
.aero-win.resizing {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.glass {
|
||||
@@ -507,13 +574,20 @@ onBeforeUnmount(() => {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Idle: slide user-input down and fade out */
|
||||
/* Idle: slide user-input down and fade out (only if empty) */
|
||||
.aero-win:not(.chrome-visible) .content :deep(.user-input) {
|
||||
opacity: 0 !important;
|
||||
transform: translateY(100%) !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Keep user-input visible when textarea has text */
|
||||
.aero-win:not(.chrome-visible) .content :deep(.user-input:has(.input-field:not(:placeholder-shown))) {
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Idle: also hide selection bar */
|
||||
.aero-win:not(.chrome-visible) .content :deep(.selection-bar) {
|
||||
opacity: 0 !important;
|
||||
@@ -643,6 +717,16 @@ onBeforeUnmount(() => {
|
||||
color: #a5b4fc;
|
||||
}
|
||||
|
||||
.window-controls .size-btn {
|
||||
color: #0ea5e9;
|
||||
}
|
||||
|
||||
.window-controls .size-btn:hover {
|
||||
color: #38bdf8;
|
||||
background: rgba(14, 165, 233, 0.15);
|
||||
border-color: rgba(14, 165, 233, 0.25);
|
||||
}
|
||||
|
||||
.window-controls button.x:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
border-color: rgba(239, 68, 68, 0.4);
|
||||
@@ -772,6 +856,21 @@ onBeforeUnmount(() => {
|
||||
/* Background handled by BackgroundPixelArt component */
|
||||
}
|
||||
|
||||
/* Dark readability overlay: between ocean bg (z-index:0) and chat (z-index:1) */
|
||||
.readability-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.35s ease;
|
||||
}
|
||||
|
||||
.aero-win.chrome-visible .readability-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Override ChatContainer backgrounds: glass-transparent */
|
||||
.content :deep(.chat-container) {
|
||||
background: transparent !important;
|
||||
|
||||
Reference in New Issue
Block a user