fix header name, modulize windowmanager, fix ui size bug
This commit is contained in:
		
							parent
							
								
									a18e93583f
								
							
						
					
					
						commit
						12a07b8607
					
				@ -70,6 +70,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "optionalDependencies": {
 | 
			
		||||
        "@img/sharp-darwin-x64": "^0.34.2",
 | 
			
		||||
        "@img/sharp-libvips-darwin-x64": "^1.1.0"
 | 
			
		||||
        "@img/sharp-libvips-darwin-x64": "^1.1.0",
 | 
			
		||||
        "electron-liquid-glass": "^1.0.1"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -248,6 +248,30 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            text-align: left;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) .container,
 | 
			
		||||
        :host-context(body.has-glass) .api-input,
 | 
			
		||||
        :host-context(body.has-glass) .provider-select,
 | 
			
		||||
        :host-context(body.has-glass) .action-button,
 | 
			
		||||
        :host-context(body.has-glass) .close-button {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* 가상 레이어·그라데이션 테두리 제거 */
 | 
			
		||||
        :host-context(body.has-glass) .container::after,
 | 
			
		||||
        :host-context(body.has-glass) .action-button::after {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* hover/active 때 다시 생기는 배경도 차단 */
 | 
			
		||||
        :host-context(body.has-glass) .action-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .provider-select:hover,
 | 
			
		||||
        :host-context(body.has-glass) .close-button:hover {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,18 @@
 | 
			
		||||
import './AppHeader.js';
 | 
			
		||||
import './MainHeader.js';
 | 
			
		||||
import './ApiKeyHeader.js';
 | 
			
		||||
import './PermissionSetup.js';
 | 
			
		||||
import './PermissionHeader.js';
 | 
			
		||||
 | 
			
		||||
class HeaderTransitionManager {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.headerContainer      = document.getElementById('header-container');
 | 
			
		||||
        this.currentHeaderType    = null;   // 'apikey' | 'app' | 'permission'
 | 
			
		||||
        this.currentHeaderType    = null;   // 'apikey' | 'main' | 'permission'
 | 
			
		||||
        this.apiKeyHeader         = null;
 | 
			
		||||
        this.appHeader            = null;
 | 
			
		||||
        this.permissionSetup      = null;
 | 
			
		||||
        this.mainHeader            = null;
 | 
			
		||||
        this.permissionHeader      = null;
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * only one header window is allowed
 | 
			
		||||
         * @param {'apikey'|'app'|'permission'} type
 | 
			
		||||
         * @param {'apikey'|'main'|'permission'} type
 | 
			
		||||
         */
 | 
			
		||||
        this.ensureHeader = (type) => {
 | 
			
		||||
            if (this.currentHeaderType === type) return;
 | 
			
		||||
@ -20,21 +20,21 @@ class HeaderTransitionManager {
 | 
			
		||||
            this.headerContainer.innerHTML = '';
 | 
			
		||||
            
 | 
			
		||||
            this.apiKeyHeader = null;
 | 
			
		||||
            this.appHeader = null;
 | 
			
		||||
            this.permissionSetup = null;
 | 
			
		||||
            this.mainHeader = null;
 | 
			
		||||
            this.permissionHeader = null;
 | 
			
		||||
 | 
			
		||||
            // Create new header element
 | 
			
		||||
            if (type === 'apikey') {
 | 
			
		||||
                this.apiKeyHeader = document.createElement('apikey-header');
 | 
			
		||||
                this.headerContainer.appendChild(this.apiKeyHeader);
 | 
			
		||||
            } else if (type === 'permission') {
 | 
			
		||||
                this.permissionSetup = document.createElement('permission-setup');
 | 
			
		||||
                this.permissionSetup.continueCallback = () => this.transitionToAppHeader();
 | 
			
		||||
                this.headerContainer.appendChild(this.permissionSetup);
 | 
			
		||||
                this.permissionHeader = document.createElement('permission-setup');
 | 
			
		||||
                this.permissionHeader.continueCallback = () => this.transitionToMainHeader();
 | 
			
		||||
                this.headerContainer.appendChild(this.permissionHeader);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.appHeader = document.createElement('app-header');
 | 
			
		||||
                this.headerContainer.appendChild(this.appHeader);
 | 
			
		||||
                this.appHeader.startSlideInAnimation?.();
 | 
			
		||||
                this.mainHeader = document.createElement('main-header');
 | 
			
		||||
                this.headerContainer.appendChild(this.mainHeader);
 | 
			
		||||
                this.mainHeader.startSlideInAnimation?.();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.currentHeaderType = type;
 | 
			
		||||
@ -87,16 +87,16 @@ class HeaderTransitionManager {
 | 
			
		||||
        const { isLoggedIn, hasApiKey } = userState;
 | 
			
		||||
 | 
			
		||||
        if (isLoggedIn) {
 | 
			
		||||
            // Firebase user: Check permissions, then show App or Permission Setup
 | 
			
		||||
            // Firebase user: Check permissions, then show Main or Permission header
 | 
			
		||||
            const permissionResult = await this.checkPermissions();
 | 
			
		||||
            if (permissionResult.success) {
 | 
			
		||||
                this.transitionToAppHeader();
 | 
			
		||||
                this.transitionToMainHeader();
 | 
			
		||||
            } else {
 | 
			
		||||
                this.transitionToPermissionSetup();
 | 
			
		||||
                this.transitionToPermissionHeader();
 | 
			
		||||
            }
 | 
			
		||||
        } else if (hasApiKey) {
 | 
			
		||||
            // API Key only user: Skip permission check, go directly to App
 | 
			
		||||
            this.transitionToAppHeader();
 | 
			
		||||
            // API Key only user: Skip permission check, go directly to Main
 | 
			
		||||
            this.transitionToMainHeader();
 | 
			
		||||
        } else {
 | 
			
		||||
            // No auth at all
 | 
			
		||||
            await this._resizeForApiKey();
 | 
			
		||||
@ -104,7 +104,7 @@ class HeaderTransitionManager {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async transitionToPermissionSetup() {
 | 
			
		||||
    async transitionToPermissionHeader() {
 | 
			
		||||
        // Prevent duplicate transitions
 | 
			
		||||
        if (this.currentHeaderType === 'permission') {
 | 
			
		||||
            console.log('[HeaderController] Already showing permission setup, skipping transition');
 | 
			
		||||
@ -123,7 +123,7 @@ class HeaderTransitionManager {
 | 
			
		||||
                    const permissionResult = await this.checkPermissions();
 | 
			
		||||
                    if (permissionResult.success) {
 | 
			
		||||
                        // Skip permission setup if already granted
 | 
			
		||||
                        this.transitionToAppHeader();
 | 
			
		||||
                        this.transitionToMainHeader();
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
@ -134,24 +134,24 @@ class HeaderTransitionManager {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this._resizeForPermissionSetup();
 | 
			
		||||
        await this._resizeForPermissionHeader();
 | 
			
		||||
        this.ensureHeader('permission');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async transitionToAppHeader(animate = true) {
 | 
			
		||||
        if (this.currentHeaderType === 'app') {
 | 
			
		||||
            return this._resizeForApp();
 | 
			
		||||
    async transitionToMainHeader(animate = true) {
 | 
			
		||||
        if (this.currentHeaderType === 'main') {
 | 
			
		||||
            return this._resizeForMain();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this._resizeForApp();
 | 
			
		||||
        this.ensureHeader('app');
 | 
			
		||||
        await this._resizeForMain();
 | 
			
		||||
        this.ensureHeader('main');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _resizeForApp() {
 | 
			
		||||
    _resizeForMain() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        return window
 | 
			
		||||
            .require('electron')
 | 
			
		||||
            .ipcRenderer.invoke('resize-header-window', { width: 353, height: 60 })
 | 
			
		||||
            .ipcRenderer.invoke('resize-header-window', { width: 353, height: 47 })
 | 
			
		||||
            .catch(() => {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -163,7 +163,7 @@ class HeaderTransitionManager {
 | 
			
		||||
            .catch(() => {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _resizeForPermissionSetup() {
 | 
			
		||||
    async _resizeForPermissionHeader() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        return window
 | 
			
		||||
            .require('electron')
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
 | 
			
		||||
 | 
			
		||||
export class AppHeader extends LitElement {
 | 
			
		||||
export class MainHeader extends LitElement {
 | 
			
		||||
    static properties = {
 | 
			
		||||
        isSessionActive: { type: Boolean, state: true },
 | 
			
		||||
    };
 | 
			
		||||
@ -292,6 +292,58 @@ export class AppHeader extends LitElement {
 | 
			
		||||
            width: 16px;
 | 
			
		||||
            height: 16px;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) .header,
 | 
			
		||||
        :host-context(body.has-glass) .listen-button,
 | 
			
		||||
        :host-context(body.has-glass) .header-actions,
 | 
			
		||||
        :host-context(body.has-glass) .settings-button {
 | 
			
		||||
            /* 배경·블러·그림자 전부 제거 */
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) .icon-box {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* 장식용 before/after 레이어와 버튼 오버레이 비활성화 */
 | 
			
		||||
        :host-context(body.has-glass) .header::before,
 | 
			
		||||
        :host-context(body.has-glass) .header::after,
 | 
			
		||||
        :host-context(body.has-glass) .listen-button::before,
 | 
			
		||||
        :host-context(body.has-glass) .listen-button::after {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* hover 때 의도치 않게 생기는 배경도 차단 */
 | 
			
		||||
        :host-context(body.has-glass) .header-actions:hover,
 | 
			
		||||
        :host-context(body.has-glass) .settings-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .listen-button:hover::before {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) * {
 | 
			
		||||
            animation: none !important;
 | 
			
		||||
            transition: none !important;
 | 
			
		||||
            transform: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* 2) pill 형태·아이콘 박스 둥근 모서리 평면화 (선택) */
 | 
			
		||||
        :host-context(body.has-glass) .header,
 | 
			
		||||
        :host-context(body.has-glass) .listen-button,
 | 
			
		||||
        :host-context(body.has-glass) .header-actions,
 | 
			
		||||
        :host-context(body.has-glass) .settings-button,
 | 
			
		||||
        :host-context(body.has-glass) .icon-box {
 | 
			
		||||
            border-radius: 0 !important;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) {
 | 
			
		||||
            animation: none !important;
 | 
			
		||||
            transition: none !important;
 | 
			
		||||
            transform: none !important;
 | 
			
		||||
            will-change: auto !important;
 | 
			
		||||
        }
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
@ -362,7 +414,7 @@ export class AppHeader extends LitElement {
 | 
			
		||||
 | 
			
		||||
    toggleVisibility() {
 | 
			
		||||
        if (this.isAnimating) {
 | 
			
		||||
            console.log('[AppHeader] Animation already in progress, ignoring toggle');
 | 
			
		||||
            console.log('[MainHeader] Animation already in progress, ignoring toggle');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
@ -432,7 +484,7 @@ export class AppHeader extends LitElement {
 | 
			
		||||
        } else if (this.classList.contains('sliding-in')) {
 | 
			
		||||
            this.classList.remove('sliding-in');
 | 
			
		||||
            this.hasSlidIn = true;
 | 
			
		||||
            console.log('[AppHeader] Slide-in animation completed');
 | 
			
		||||
            console.log('[MainHeader] Slide-in animation completed');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -484,7 +536,7 @@ export class AppHeader extends LitElement {
 | 
			
		||||
        if (this.wasJustDragged) return;
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            console.log(`[AppHeader] showWindow('${name}') called at ${Date.now()}`);
 | 
			
		||||
            console.log(`[MainHeader] showWindow('${name}') called at ${Date.now()}`);
 | 
			
		||||
            
 | 
			
		||||
            ipcRenderer.send('cancel-hide-window', name);
 | 
			
		||||
 | 
			
		||||
@ -508,7 +560,7 @@ export class AppHeader extends LitElement {
 | 
			
		||||
    hideWindow(name) {
 | 
			
		||||
        if (this.wasJustDragged) return;
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            console.log(`[AppHeader] hideWindow('${name}') called at ${Date.now()}`);
 | 
			
		||||
            console.log(`[MainHeader] hideWindow('${name}') called at ${Date.now()}`);
 | 
			
		||||
            window.require('electron').ipcRenderer.send('hide-window', name);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -590,4 +642,4 @@ export class AppHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define('app-header', AppHeader);
 | 
			
		||||
customElements.define('main-header', MainHeader);
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { LitElement, html, css } from '../assets/lit-core-2.7.4.min.js';
 | 
			
		||||
 | 
			
		||||
export class PermissionSetup extends LitElement {
 | 
			
		||||
export class PermissionHeader extends LitElement {
 | 
			
		||||
    static styles = css`
 | 
			
		||||
        :host {
 | 
			
		||||
            display: block;
 | 
			
		||||
@ -237,6 +237,30 @@ export class PermissionSetup extends LitElement {
 | 
			
		||||
            background: rgba(255, 255, 255, 0.2);
 | 
			
		||||
            cursor: not-allowed;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) .container,
 | 
			
		||||
        :host-context(body.has-glass) .action-button,
 | 
			
		||||
        :host-context(body.has-glass) .continue-button,
 | 
			
		||||
        :host-context(body.has-glass) .close-button {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* Remove gradient borders / pseudo layers */
 | 
			
		||||
        :host-context(body.has-glass) .container::after,
 | 
			
		||||
        :host-context(body.has-glass) .action-button::after,
 | 
			
		||||
        :host-context(body.has-glass) .continue-button::after {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* Prevent background reappearing on hover/active */
 | 
			
		||||
        :host-context(body.has-glass) .action-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .continue-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .close-button:hover {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    static properties = {
 | 
			
		||||
@ -337,7 +361,7 @@ export class PermissionSetup extends LitElement {
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const permissions = await ipcRenderer.invoke('check-system-permissions');
 | 
			
		||||
            console.log('[PermissionSetup] Permission check result:', permissions);
 | 
			
		||||
            console.log('[PermissionHeader] Permission check result:', permissions);
 | 
			
		||||
            
 | 
			
		||||
            const prevMic = this.microphoneGranted;
 | 
			
		||||
            const prevScreen = this.screenGranted;
 | 
			
		||||
@ -347,7 +371,7 @@ export class PermissionSetup extends LitElement {
 | 
			
		||||
            
 | 
			
		||||
            // if permissions changed == UI update
 | 
			
		||||
            if (prevMic !== this.microphoneGranted || prevScreen !== this.screenGranted) {
 | 
			
		||||
                console.log('[PermissionSetup] Permission status changed, updating UI');
 | 
			
		||||
                console.log('[PermissionHeader] Permission status changed, updating UI');
 | 
			
		||||
                this.requestUpdate();
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
@ -355,11 +379,11 @@ export class PermissionSetup extends LitElement {
 | 
			
		||||
            if (this.microphoneGranted === 'granted' && 
 | 
			
		||||
                this.screenGranted === 'granted' && 
 | 
			
		||||
                this.continueCallback) {
 | 
			
		||||
                console.log('[PermissionSetup] All permissions granted, proceeding automatically');
 | 
			
		||||
                console.log('[PermissionHeader] All permissions granted, proceeding automatically');
 | 
			
		||||
                setTimeout(() => this.handleContinue(), 500);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[PermissionSetup] Error checking permissions:', error);
 | 
			
		||||
            console.error('[PermissionHeader] Error checking permissions:', error);
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.isChecking = false;
 | 
			
		||||
        }
 | 
			
		||||
@ -368,12 +392,12 @@ export class PermissionSetup extends LitElement {
 | 
			
		||||
    async handleMicrophoneClick() {
 | 
			
		||||
        if (!window.require || this.microphoneGranted === 'granted' || this.wasJustDragged) return;
 | 
			
		||||
        
 | 
			
		||||
        console.log('[PermissionSetup] Requesting microphone permission...');
 | 
			
		||||
        console.log('[PermissionHeader] Requesting microphone permission...');
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const result = await ipcRenderer.invoke('check-system-permissions');
 | 
			
		||||
            console.log('[PermissionSetup] Microphone permission result:', result);
 | 
			
		||||
            console.log('[PermissionHeader] Microphone permission result:', result);
 | 
			
		||||
            
 | 
			
		||||
            if (result.microphone === 'granted') {
 | 
			
		||||
                this.microphoneGranted = 'granted';
 | 
			
		||||
@ -394,19 +418,19 @@ export class PermissionSetup extends LitElement {
 | 
			
		||||
            // Check permissions again after a delay
 | 
			
		||||
            // setTimeout(() => this.checkPermissions(), 1000);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[PermissionSetup] Error requesting microphone permission:', error);
 | 
			
		||||
            console.error('[PermissionHeader] Error requesting microphone permission:', error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleScreenClick() {
 | 
			
		||||
        if (!window.require || this.screenGranted === 'granted' || this.wasJustDragged) return;
 | 
			
		||||
        
 | 
			
		||||
        console.log('[PermissionSetup] Checking screen recording permission...');
 | 
			
		||||
        console.log('[PermissionHeader] Checking screen recording permission...');
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const permissions = await ipcRenderer.invoke('check-system-permissions');
 | 
			
		||||
            console.log('[PermissionSetup] Screen permission check result:', permissions);
 | 
			
		||||
            console.log('[PermissionHeader] Screen permission check result:', permissions);
 | 
			
		||||
            
 | 
			
		||||
            if (permissions.screen === 'granted') {
 | 
			
		||||
                this.screenGranted = 'granted';
 | 
			
		||||
@ -414,7 +438,7 @@ export class PermissionSetup extends LitElement {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (permissions.screen === 'not-determined' || permissions.screen === 'denied' || permissions.screen === 'unknown' || permissions.screen === 'restricted') {
 | 
			
		||||
            console.log('[PermissionSetup] Opening screen recording preferences...');
 | 
			
		||||
            console.log('[PermissionHeader] Opening screen recording preferences...');
 | 
			
		||||
            await ipcRenderer.invoke('open-system-preferences', 'screen-recording');
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
@ -422,7 +446,7 @@ export class PermissionSetup extends LitElement {
 | 
			
		||||
            // (This may not execute if app restarts after permission grant)
 | 
			
		||||
            // setTimeout(() => this.checkPermissions(), 2000);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[PermissionSetup] Error opening screen recording preferences:', error);
 | 
			
		||||
            console.error('[PermissionHeader] Error opening screen recording preferences:', error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -436,9 +460,9 @@ export class PermissionSetup extends LitElement {
 | 
			
		||||
                const { ipcRenderer } = window.require('electron');
 | 
			
		||||
                try {
 | 
			
		||||
                    await ipcRenderer.invoke('mark-permissions-completed');
 | 
			
		||||
                    console.log('[PermissionSetup] Marked permissions as completed');
 | 
			
		||||
                    console.log('[PermissionHeader] Marked permissions as completed');
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error('[PermissionSetup] Error marking permissions as completed:', error);
 | 
			
		||||
                    console.error('[PermissionHeader] Error marking permissions as completed:', error);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
@ -530,4 +554,4 @@ export class PermissionSetup extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define('permission-setup', PermissionSetup); 
 | 
			
		||||
customElements.define('permission-setup', PermissionHeader); 
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
 | 
			
		||||
import { CustomizeView } from '../features/customize/CustomizeView.js';
 | 
			
		||||
import { AssistantView } from '../features/listen/AssistantView.js';
 | 
			
		||||
import { OnboardingView } from '../features/onboarding/OnboardingView.js';
 | 
			
		||||
import { AskView } from '../features/ask/AskView.js';
 | 
			
		||||
 | 
			
		||||
import '../features/listen/renderer.js';
 | 
			
		||||
@ -11,6 +10,7 @@ export class PickleGlassApp extends LitElement {
 | 
			
		||||
        :host {
 | 
			
		||||
            display: block;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 100%;
 | 
			
		||||
            color: var(--text-color);
 | 
			
		||||
            background: transparent;
 | 
			
		||||
            border-radius: 7px;
 | 
			
		||||
@ -19,11 +19,13 @@ export class PickleGlassApp extends LitElement {
 | 
			
		||||
        assistant-view {
 | 
			
		||||
            display: block;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 100%;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ask-view, customize-view, history-view, help-view, onboarding-view, setup-view {
 | 
			
		||||
            display: block;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 100%;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
@ -301,5 +301,11 @@
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        </script>
 | 
			
		||||
        <script>
 | 
			
		||||
            const params = new URLSearchParams(window.location.search);
 | 
			
		||||
            if (params.get('glass') === 'true') {
 | 
			
		||||
                document.body.classList.add('has-glass');
 | 
			
		||||
            }
 | 
			
		||||
        </script>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
@ -15,10 +15,14 @@
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
        <div id="header-container" tabindex="0" style="outline: none;">
 | 
			
		||||
            <!-- <apikey-header id="apikey-header" style="display: none;"></apikey-header>
 | 
			
		||||
            <app-header id="app-header" style="display: none;"></app-header> -->
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <script type="module" src="../../public/build/header.js"></script>
 | 
			
		||||
        <script>
 | 
			
		||||
            const params = new URLSearchParams(window.location.search);
 | 
			
		||||
            if (params.get('glass') === 'true') {
 | 
			
		||||
                document.body.classList.add('has-glass');
 | 
			
		||||
            }
 | 
			
		||||
        </script>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										312
									
								
								src/electron/smoothMovementManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								src/electron/smoothMovementManager.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,312 @@
 | 
			
		||||
const { screen } = require('electron');
 | 
			
		||||
 | 
			
		||||
class SmoothMovementManager {
 | 
			
		||||
    constructor(windowPool, getDisplayById, getCurrentDisplay, updateLayout) {
 | 
			
		||||
        this.windowPool = windowPool;
 | 
			
		||||
        this.getDisplayById = getDisplayById;
 | 
			
		||||
        this.getCurrentDisplay = getCurrentDisplay;
 | 
			
		||||
        this.updateLayout = updateLayout;
 | 
			
		||||
        this.stepSize = 80;
 | 
			
		||||
        this.animationDuration = 300;
 | 
			
		||||
        this.headerPosition = { x: 0, y: 0 };
 | 
			
		||||
        this.isAnimating = false;
 | 
			
		||||
        this.hiddenPosition = null;
 | 
			
		||||
        this.lastVisiblePosition = null;
 | 
			
		||||
        this.currentDisplayId = null;
 | 
			
		||||
        this.animationFrameId = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param {BrowserWindow} win
 | 
			
		||||
     * @returns {boolean}
 | 
			
		||||
     */
 | 
			
		||||
    _isWindowValid(win) {
 | 
			
		||||
        if (!win || win.isDestroyed()) {
 | 
			
		||||
            if (this.isAnimating) {
 | 
			
		||||
                console.warn('[MovementManager] Window destroyed mid-animation. Halting.');
 | 
			
		||||
                this.isAnimating = false;
 | 
			
		||||
                if (this.animationFrameId) {
 | 
			
		||||
                    clearTimeout(this.animationFrameId);
 | 
			
		||||
                    this.animationFrameId = null;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    moveToDisplay(displayId) {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
 | 
			
		||||
 | 
			
		||||
        const targetDisplay = this.getDisplayById(displayId);
 | 
			
		||||
        if (!targetDisplay) return;
 | 
			
		||||
 | 
			
		||||
        const currentBounds = header.getBounds();
 | 
			
		||||
        const currentDisplay = this.getCurrentDisplay(header);
 | 
			
		||||
 | 
			
		||||
        if (currentDisplay.id === targetDisplay.id) return;
 | 
			
		||||
 | 
			
		||||
        const relativeX = (currentBounds.x - currentDisplay.workArea.x) / currentDisplay.workAreaSize.width;
 | 
			
		||||
        const relativeY = (currentBounds.y - currentDisplay.workArea.y) / currentDisplay.workAreaSize.height;
 | 
			
		||||
        const targetX = targetDisplay.workArea.x + targetDisplay.workAreaSize.width * relativeX;
 | 
			
		||||
        const targetY = targetDisplay.workArea.y + targetDisplay.workAreaSize.height * relativeY;
 | 
			
		||||
 | 
			
		||||
        const finalX = Math.max(targetDisplay.workArea.x, Math.min(targetDisplay.workArea.x + targetDisplay.workAreaSize.width - currentBounds.width, targetX));
 | 
			
		||||
        const finalY = Math.max(targetDisplay.workArea.y, Math.min(targetDisplay.workArea.y + targetDisplay.workAreaSize.height - currentBounds.height, targetY));
 | 
			
		||||
 | 
			
		||||
        this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
 | 
			
		||||
        this.animateToPosition(header, finalX, finalY);
 | 
			
		||||
        this.currentDisplayId = targetDisplay.id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hideToEdge(edge, callback) {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
 | 
			
		||||
    
 | 
			
		||||
        const currentBounds = header.getBounds();
 | 
			
		||||
        const display = this.getCurrentDisplay(header);
 | 
			
		||||
    
 | 
			
		||||
        if (
 | 
			
		||||
            !currentBounds || typeof currentBounds.x !== 'number' || typeof currentBounds.y !== 'number' ||
 | 
			
		||||
            !display || !display.workArea || !display.workAreaSize ||
 | 
			
		||||
            typeof display.workArea.x !== 'number' || typeof display.workArea.y !== 'number' ||
 | 
			
		||||
            typeof display.workAreaSize.width !== 'number' || typeof display.workAreaSize.height !== 'number'
 | 
			
		||||
        ) {
 | 
			
		||||
            console.error('[MovementManager] Invalid bounds or display info for hideToEdge. Aborting.');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
        this.lastVisiblePosition = { x: currentBounds.x, y: currentBounds.y };
 | 
			
		||||
        this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
 | 
			
		||||
    
 | 
			
		||||
        const { width: screenWidth, height: screenHeight } = display.workAreaSize;
 | 
			
		||||
        const { x: workAreaX, y: workAreaY } = display.workArea;
 | 
			
		||||
        
 | 
			
		||||
        let targetX = this.headerPosition.x;
 | 
			
		||||
        let targetY = this.headerPosition.y;
 | 
			
		||||
    
 | 
			
		||||
        switch (edge) {
 | 
			
		||||
            case 'top': targetY = workAreaY - currentBounds.height - 20; break;
 | 
			
		||||
            case 'bottom': targetY = workAreaY + screenHeight + 20; break;
 | 
			
		||||
            case 'left': targetX = workAreaX - currentBounds.width - 20; break;
 | 
			
		||||
            case 'right': targetX = workAreaX + screenWidth + 20; break;
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
        this.hiddenPosition = { x: targetX, y: targetY, edge };
 | 
			
		||||
        this.isAnimating = true;
 | 
			
		||||
        const startX = this.headerPosition.x;
 | 
			
		||||
        const startY = this.headerPosition.y;
 | 
			
		||||
        const duration = 400;
 | 
			
		||||
        const startTime = Date.now();
 | 
			
		||||
    
 | 
			
		||||
        const animate = () => {
 | 
			
		||||
            if (!this._isWindowValid(header)) return;
 | 
			
		||||
 | 
			
		||||
            const elapsed = Date.now() - startTime;
 | 
			
		||||
            const progress = Math.min(elapsed / duration, 1);
 | 
			
		||||
            const eased = progress * progress * progress;
 | 
			
		||||
            const currentX = startX + (targetX - startX) * eased;
 | 
			
		||||
            const currentY = startY + (targetY - startY) * eased;
 | 
			
		||||
            
 | 
			
		||||
            if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
 | 
			
		||||
                this.isAnimating = false;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
    
 | 
			
		||||
            if (!this._isWindowValid(header)) return;
 | 
			
		||||
            header.setPosition(Math.round(currentX), Math.round(currentY));
 | 
			
		||||
    
 | 
			
		||||
            if (progress < 1) {
 | 
			
		||||
                this.animationFrameId = setTimeout(animate, 8);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.animationFrameId = null;
 | 
			
		||||
                this.headerPosition = { x: targetX, y: targetY };
 | 
			
		||||
                if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
 | 
			
		||||
                    if (!this._isWindowValid(header)) return;
 | 
			
		||||
                    header.setPosition(Math.round(targetX), Math.round(targetY));
 | 
			
		||||
                }
 | 
			
		||||
                this.isAnimating = false;
 | 
			
		||||
                if (typeof callback === 'function') callback();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        animate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showFromEdge(callback) {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (
 | 
			
		||||
            !this._isWindowValid(header) || this.isAnimating ||
 | 
			
		||||
            !this.hiddenPosition || !this.lastVisiblePosition ||
 | 
			
		||||
            typeof this.hiddenPosition.x !== 'number' || typeof this.hiddenPosition.y !== 'number' ||
 | 
			
		||||
            typeof this.lastVisiblePosition.x !== 'number' || typeof this.lastVisiblePosition.y !== 'number'
 | 
			
		||||
        ) {
 | 
			
		||||
            console.error('[MovementManager] Invalid state for showFromEdge. Aborting.');
 | 
			
		||||
            this.isAnimating = false;
 | 
			
		||||
            this.hiddenPosition = null;
 | 
			
		||||
            this.lastVisiblePosition = null;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this._isWindowValid(header)) return;
 | 
			
		||||
        header.setPosition(this.hiddenPosition.x, this.hiddenPosition.y);
 | 
			
		||||
        
 | 
			
		||||
        this.headerPosition = { x: this.hiddenPosition.x, y: this.hiddenPosition.y };
 | 
			
		||||
        const targetX = this.lastVisiblePosition.x;
 | 
			
		||||
        const targetY = this.lastVisiblePosition.y;
 | 
			
		||||
        this.isAnimating = true;
 | 
			
		||||
        const startX = this.headerPosition.x;
 | 
			
		||||
        const startY = this.headerPosition.y;
 | 
			
		||||
        const duration = 500;
 | 
			
		||||
        const startTime = Date.now();
 | 
			
		||||
 | 
			
		||||
        const animate = () => {
 | 
			
		||||
            if (!this._isWindowValid(header)) return;
 | 
			
		||||
 | 
			
		||||
            const elapsed = Date.now() - startTime;
 | 
			
		||||
            const progress = Math.min(elapsed / duration, 1);
 | 
			
		||||
            const c1 = 1.70158;
 | 
			
		||||
            const c3 = c1 + 1;
 | 
			
		||||
            const eased = 1 + c3 * Math.pow(progress - 1, 3) + c1 * Math.pow(progress - 1, 2);
 | 
			
		||||
            const currentX = startX + (targetX - startX) * eased;
 | 
			
		||||
            const currentY = startY + (targetY - startY) * eased;
 | 
			
		||||
            if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
 | 
			
		||||
                this.isAnimating = false;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!this._isWindowValid(header)) return;
 | 
			
		||||
            header.setPosition(Math.round(currentX), Math.round(currentY));
 | 
			
		||||
 | 
			
		||||
            if (progress < 1) {
 | 
			
		||||
                this.animationFrameId = setTimeout(animate, 8);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.animationFrameId = null;
 | 
			
		||||
                this.headerPosition = { x: targetX, y: targetY };
 | 
			
		||||
                if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
 | 
			
		||||
                    if (!this._isWindowValid(header)) return;
 | 
			
		||||
                    header.setPosition(Math.round(targetX), Math.round(targetY));
 | 
			
		||||
                }
 | 
			
		||||
                this.isAnimating = false;
 | 
			
		||||
                this.hiddenPosition = null;
 | 
			
		||||
                this.lastVisiblePosition = null;
 | 
			
		||||
                if (callback) callback();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        animate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    moveStep(direction) {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
 | 
			
		||||
 | 
			
		||||
        const currentBounds = header.getBounds();
 | 
			
		||||
        this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
 | 
			
		||||
        let targetX = this.headerPosition.x;
 | 
			
		||||
        let targetY = this.headerPosition.y;
 | 
			
		||||
 | 
			
		||||
        switch (direction) {
 | 
			
		||||
            case 'left': targetX -= this.stepSize; break;
 | 
			
		||||
            case 'right': targetX += this.stepSize; break;
 | 
			
		||||
            case 'up': targetY -= this.stepSize; break;
 | 
			
		||||
            case 'down': targetY += this.stepSize; break;
 | 
			
		||||
            default: return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const displays = screen.getAllDisplays();
 | 
			
		||||
        let validPosition = displays.some(d => (
 | 
			
		||||
            targetX >= d.workArea.x && targetX + currentBounds.width <= d.workArea.x + d.workArea.width &&
 | 
			
		||||
            targetY >= d.workArea.y && targetY + currentBounds.height <= d.workArea.y + d.workArea.height
 | 
			
		||||
        ));
 | 
			
		||||
 | 
			
		||||
        if (!validPosition) {
 | 
			
		||||
            const nearestDisplay = screen.getDisplayNearestPoint({ x: targetX, y: targetY });
 | 
			
		||||
            const { x, y, width, height } = nearestDisplay.workArea;
 | 
			
		||||
            targetX = Math.max(x, Math.min(x + width - currentBounds.width, targetX));
 | 
			
		||||
            targetY = Math.max(y, Math.min(y + height - currentBounds.height, targetY));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (targetX === this.headerPosition.x && targetY === this.headerPosition.y) return;
 | 
			
		||||
        this.animateToPosition(header, targetX, targetY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    animateToPosition(header, targetX, targetY) {
 | 
			
		||||
        if (!this._isWindowValid(header)) return;
 | 
			
		||||
        
 | 
			
		||||
        this.isAnimating = true;
 | 
			
		||||
        const startX = this.headerPosition.x;
 | 
			
		||||
        const startY = this.headerPosition.y;
 | 
			
		||||
        const startTime = Date.now();
 | 
			
		||||
 | 
			
		||||
        if (!Number.isFinite(targetX) || !Number.isFinite(targetY) || !Number.isFinite(startX) || !Number.isFinite(startY)) {
 | 
			
		||||
            this.isAnimating = false;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const animate = () => {
 | 
			
		||||
            if (!this._isWindowValid(header)) return;
 | 
			
		||||
 | 
			
		||||
            const elapsed = Date.now() - startTime;
 | 
			
		||||
            const progress = Math.min(elapsed / this.animationDuration, 1);
 | 
			
		||||
            const eased = 1 - Math.pow(1 - progress, 3);
 | 
			
		||||
            const currentX = startX + (targetX - startX) * eased;
 | 
			
		||||
            const currentY = startY + (targetY - startY) * eased;
 | 
			
		||||
 | 
			
		||||
            if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
 | 
			
		||||
                this.isAnimating = false;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!this._isWindowValid(header)) return;
 | 
			
		||||
            header.setPosition(Math.round(currentX), Math.round(currentY));
 | 
			
		||||
 | 
			
		||||
            if (progress < 1) {
 | 
			
		||||
                this.animationFrameId = setTimeout(animate, 8);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.animationFrameId = null;
 | 
			
		||||
                this.headerPosition = { x: targetX, y: targetY };
 | 
			
		||||
                if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
 | 
			
		||||
                    if (!this._isWindowValid(header)) return;
 | 
			
		||||
                    header.setPosition(Math.round(targetX), Math.round(targetY));
 | 
			
		||||
                }
 | 
			
		||||
                this.isAnimating = false;
 | 
			
		||||
                this.updateLayout();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        animate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    moveToEdge(direction) {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
 | 
			
		||||
 | 
			
		||||
        const display = this.getCurrentDisplay(header);
 | 
			
		||||
        const { width, height } = display.workAreaSize;
 | 
			
		||||
        const { x: workAreaX, y: workAreaY } = display.workArea;
 | 
			
		||||
        const headerBounds = header.getBounds();
 | 
			
		||||
        const currentBounds = header.getBounds();
 | 
			
		||||
        let targetX = currentBounds.x;
 | 
			
		||||
        let targetY = currentBounds.y;
 | 
			
		||||
 | 
			
		||||
        switch (direction) {
 | 
			
		||||
            case 'left': targetX = workAreaX; break;
 | 
			
		||||
            case 'right': targetX = workAreaX + width - headerBounds.width; break;
 | 
			
		||||
            case 'up': targetY = workAreaY; break;
 | 
			
		||||
            case 'down': targetY = workAreaY + height - headerBounds.height; break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
 | 
			
		||||
        this.animateToPosition(header, targetX, targetY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
        if (this.animationFrameId) {
 | 
			
		||||
            clearTimeout(this.animationFrameId);
 | 
			
		||||
            this.animationFrameId = null;
 | 
			
		||||
        }
 | 
			
		||||
        this.isAnimating = false;
 | 
			
		||||
        console.log('[Movement] Manager destroyed');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = SmoothMovementManager;
 | 
			
		||||
							
								
								
									
										217
									
								
								src/electron/windowLayoutManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								src/electron/windowLayoutManager.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,217 @@
 | 
			
		||||
const { screen } = require('electron');
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 주어진 창이 현재 어느 디스플레이에 속해 있는지 반환합니다.
 | 
			
		||||
 * @param {BrowserWindow} window - 확인할 창 객체
 | 
			
		||||
 * @returns {Display} Electron의 Display 객체
 | 
			
		||||
 */
 | 
			
		||||
function getCurrentDisplay(window) {
 | 
			
		||||
    if (!window || window.isDestroyed()) return screen.getPrimaryDisplay();
 | 
			
		||||
 | 
			
		||||
    const windowBounds = window.getBounds();
 | 
			
		||||
    const windowCenter = {
 | 
			
		||||
        x: windowBounds.x + windowBounds.width / 2,
 | 
			
		||||
        y: windowBounds.y + windowBounds.height / 2,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return screen.getDisplayNearestPoint(windowCenter);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class WindowLayoutManager {
 | 
			
		||||
    /**
 | 
			
		||||
     * @param {Map<string, BrowserWindow>} windowPool - 관리할 창들의 맵
 | 
			
		||||
     */
 | 
			
		||||
    constructor(windowPool) {
 | 
			
		||||
        this.windowPool = windowPool;
 | 
			
		||||
        this.isUpdating = false;
 | 
			
		||||
        this.PADDING = 80;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 모든 창의 레이아웃 업데이트를 요청합니다.
 | 
			
		||||
     * 중복 실행을 방지하기 위해 isUpdating 플래그를 사용합니다.
 | 
			
		||||
     */
 | 
			
		||||
    updateLayout() {
 | 
			
		||||
        if (this.isUpdating) return;
 | 
			
		||||
        this.isUpdating = true;
 | 
			
		||||
 | 
			
		||||
        setImmediate(() => {
 | 
			
		||||
            this.positionWindows();
 | 
			
		||||
            this.isUpdating = false;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 헤더 창을 기준으로 모든 기능 창들의 위치를 계산하고 배치합니다.
 | 
			
		||||
     */
 | 
			
		||||
    positionWindows() {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!header?.getBounds) return;
 | 
			
		||||
 | 
			
		||||
        const headerBounds = header.getBounds();
 | 
			
		||||
        const display = getCurrentDisplay(header);
 | 
			
		||||
        const { width: screenWidth, height: screenHeight } = display.workAreaSize;
 | 
			
		||||
        const { x: workAreaX, y: workAreaY } = display.workArea;
 | 
			
		||||
 | 
			
		||||
        const headerCenterX = headerBounds.x - workAreaX + headerBounds.width / 2;
 | 
			
		||||
        const headerCenterY = headerBounds.y - workAreaY + headerBounds.height / 2;
 | 
			
		||||
 | 
			
		||||
        const relativeX = headerCenterX / screenWidth;
 | 
			
		||||
        const relativeY = headerCenterY / screenHeight;
 | 
			
		||||
 | 
			
		||||
        const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY);
 | 
			
		||||
 | 
			
		||||
        this.positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY);
 | 
			
		||||
        this.positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 헤더 창의 위치에 따라 기능 창들을 배치할 최적의 전략을 결정합니다.
 | 
			
		||||
     * @returns {{name: string, primary: string, secondary: string}} 레이아웃 전략
 | 
			
		||||
     */
 | 
			
		||||
    determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY) {
 | 
			
		||||
        const spaceBelow = screenHeight - (headerBounds.y + headerBounds.height);
 | 
			
		||||
        const spaceAbove = headerBounds.y;
 | 
			
		||||
        const spaceLeft = headerBounds.x;
 | 
			
		||||
        const spaceRight = screenWidth - (headerBounds.x + headerBounds.width);
 | 
			
		||||
 | 
			
		||||
        if (spaceBelow >= 400) {
 | 
			
		||||
            return { name: 'below', primary: 'below', secondary: relativeX < 0.5 ? 'right' : 'left' };
 | 
			
		||||
        } else if (spaceAbove >= 400) {
 | 
			
		||||
            return { name: 'above', primary: 'above', secondary: relativeX < 0.5 ? 'right' : 'left' };
 | 
			
		||||
        } else if (relativeX < 0.3 && spaceRight >= 800) {
 | 
			
		||||
            return { name: 'right-side', primary: 'right', secondary: spaceBelow > spaceAbove ? 'below' : 'above' };
 | 
			
		||||
        } else if (relativeX > 0.7 && spaceLeft >= 800) {
 | 
			
		||||
            return { name: 'left-side', primary: 'left', secondary: spaceBelow > spaceAbove ? 'below' : 'above' };
 | 
			
		||||
        } else {
 | 
			
		||||
            return { name: 'adaptive', primary: spaceBelow > spaceAbove ? 'below' : 'above', secondary: spaceRight > spaceLeft ? 'right' : 'left' };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 'ask'와 'listen' 창의 위치를 조정합니다.
 | 
			
		||||
     */
 | 
			
		||||
    positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) {
 | 
			
		||||
        const ask = this.windowPool.get('ask');
 | 
			
		||||
        const listen = this.windowPool.get('listen');
 | 
			
		||||
        const askVisible = ask && ask.isVisible() && !ask.isDestroyed();
 | 
			
		||||
        const listenVisible = listen && listen.isVisible() && !listen.isDestroyed();
 | 
			
		||||
 | 
			
		||||
        if (!askVisible && !listenVisible) return;
 | 
			
		||||
 | 
			
		||||
        const PAD = 8;
 | 
			
		||||
        const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2;
 | 
			
		||||
        let askBounds = askVisible ? ask.getBounds() : null;
 | 
			
		||||
        let listenBounds = listenVisible ? listen.getBounds() : null;
 | 
			
		||||
 | 
			
		||||
        if (askVisible && listenVisible) {
 | 
			
		||||
            const combinedWidth = listenBounds.width + PAD + askBounds.width;
 | 
			
		||||
            let groupStartXRel = headerCenterXRel - combinedWidth / 2;
 | 
			
		||||
            let listenXRel = groupStartXRel;
 | 
			
		||||
            let askXRel = groupStartXRel + listenBounds.width + PAD;
 | 
			
		||||
 | 
			
		||||
            if (listenXRel < PAD) {
 | 
			
		||||
                listenXRel = PAD;
 | 
			
		||||
                askXRel = listenXRel + listenBounds.width + PAD;
 | 
			
		||||
            }
 | 
			
		||||
            if (askXRel + askBounds.width > screenWidth - PAD) {
 | 
			
		||||
                askXRel = screenWidth - PAD - askBounds.width;
 | 
			
		||||
                listenXRel = askXRel - listenBounds.width - PAD;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let yRel = (strategy.primary === 'above')
 | 
			
		||||
                ? headerBounds.y - workAreaY - Math.max(askBounds.height, listenBounds.height) - PAD
 | 
			
		||||
                : headerBounds.y - workAreaY + headerBounds.height + PAD;
 | 
			
		||||
 | 
			
		||||
            listen.setBounds({ x: Math.round(listenXRel + workAreaX), y: Math.round(yRel + workAreaY), width: listenBounds.width, height: listenBounds.height });
 | 
			
		||||
            ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(yRel + workAreaY), width: askBounds.width, height: askBounds.height });
 | 
			
		||||
        } else {
 | 
			
		||||
            const win = askVisible ? ask : listen;
 | 
			
		||||
            const winBounds = askVisible ? askBounds : listenBounds;
 | 
			
		||||
            let xRel = headerCenterXRel - winBounds.width / 2;
 | 
			
		||||
            let yRel = (strategy.primary === 'above')
 | 
			
		||||
                ? headerBounds.y - workAreaY - winBounds.height - PAD
 | 
			
		||||
                : headerBounds.y - workAreaY + headerBounds.height + PAD;
 | 
			
		||||
 | 
			
		||||
            xRel = Math.max(PAD, Math.min(screenWidth - winBounds.width - PAD, xRel));
 | 
			
		||||
            yRel = Math.max(PAD, Math.min(screenHeight - winBounds.height - PAD, yRel));
 | 
			
		||||
 | 
			
		||||
            win.setBounds({ x: Math.round(xRel + workAreaX), y: Math.round(yRel + workAreaY), width: winBounds.width, height: winBounds.height });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 'settings' 창의 위치를 조정합니다.
 | 
			
		||||
     */
 | 
			
		||||
    positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) {
 | 
			
		||||
        const settings = this.windowPool.get('settings');
 | 
			
		||||
        if (!settings?.getBounds || !settings.isVisible()) return;
 | 
			
		||||
 | 
			
		||||
        if (settings.__lockedByButton) {
 | 
			
		||||
            const headerDisplay = getCurrentDisplay(this.windowPool.get('header'));
 | 
			
		||||
            const settingsDisplay = getCurrentDisplay(settings);
 | 
			
		||||
            if (headerDisplay.id !== settingsDisplay.id) {
 | 
			
		||||
                settings.__lockedByButton = false;
 | 
			
		||||
            } else {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const settingsBounds = settings.getBounds();
 | 
			
		||||
        const PAD = 5;
 | 
			
		||||
        const buttonPadding = 17;
 | 
			
		||||
        let x = headerBounds.x + headerBounds.width - settingsBounds.width - buttonPadding;
 | 
			
		||||
        let y = headerBounds.y + headerBounds.height + PAD;
 | 
			
		||||
 | 
			
		||||
        const otherVisibleWindows = [];
 | 
			
		||||
        ['listen', 'ask'].forEach(name => {
 | 
			
		||||
            const win = this.windowPool.get(name);
 | 
			
		||||
            if (win && win.isVisible() && !win.isDestroyed()) {
 | 
			
		||||
                otherVisibleWindows.push({ name, bounds: win.getBounds() });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const settingsNewBounds = { x, y, width: settingsBounds.width, height: settingsBounds.height };
 | 
			
		||||
        let hasOverlap = otherVisibleWindows.some(otherWin => this.boundsOverlap(settingsNewBounds, otherWin.bounds));
 | 
			
		||||
 | 
			
		||||
        if (hasOverlap) {
 | 
			
		||||
            x = headerBounds.x + headerBounds.width + PAD;
 | 
			
		||||
            y = headerBounds.y;
 | 
			
		||||
            if (x + settingsBounds.width > screenWidth - 10) {
 | 
			
		||||
                x = headerBounds.x - settingsBounds.width - PAD;
 | 
			
		||||
            }
 | 
			
		||||
            if (x < 10) {
 | 
			
		||||
                x = headerBounds.x + headerBounds.width - settingsBounds.width - buttonPadding;
 | 
			
		||||
                y = headerBounds.y - settingsBounds.height - PAD;
 | 
			
		||||
                if (y < 10) {
 | 
			
		||||
                    x = headerBounds.x + headerBounds.width - settingsBounds.width;
 | 
			
		||||
                    y = headerBounds.y + headerBounds.height + PAD;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        x = Math.max(workAreaX + 10, Math.min(workAreaX + screenWidth - settingsBounds.width - 10, x));
 | 
			
		||||
        y = Math.max(workAreaY + 10, Math.min(workAreaY + screenHeight - settingsBounds.height - 10, y));
 | 
			
		||||
 | 
			
		||||
        settings.setBounds({ x: Math.round(x), y: Math.round(y) });
 | 
			
		||||
        settings.moveTop();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * 두 사각형 영역이 겹치는지 확인합니다.
 | 
			
		||||
     * @param {Rectangle} bounds1
 | 
			
		||||
     * @param {Rectangle} bounds2
 | 
			
		||||
     * @returns {boolean} 겹침 여부
 | 
			
		||||
     */
 | 
			
		||||
    boundsOverlap(bounds1, bounds2) {
 | 
			
		||||
        const margin = 10;
 | 
			
		||||
        return !(
 | 
			
		||||
            bounds1.x + bounds1.width + margin < bounds2.x ||
 | 
			
		||||
            bounds2.x + bounds2.width + margin < bounds1.x ||
 | 
			
		||||
            bounds1.y + bounds1.height + margin < bounds2.y ||
 | 
			
		||||
            bounds2.y + bounds2.height + margin < bounds1.y
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = WindowLayoutManager;
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -590,6 +590,43 @@ export class AskView extends LitElement {
 | 
			
		||||
            color: rgba(255, 255, 255, 0.5);
 | 
			
		||||
            font-size: 14px;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) .ask-container,
 | 
			
		||||
        :host-context(body.has-glass) .response-header,
 | 
			
		||||
        :host-context(body.has-glass) .response-icon,
 | 
			
		||||
        :host-context(body.has-glass) .copy-button,
 | 
			
		||||
        :host-context(body.has-glass) .close-button,
 | 
			
		||||
        :host-context(body.has-glass) .line-copy-button,
 | 
			
		||||
        :host-context(body.has-glass) .text-input-container,
 | 
			
		||||
        :host-context(body.has-glass) .response-container pre,
 | 
			
		||||
        :host-context(body.has-glass) .response-container p code,
 | 
			
		||||
        :host-context(body.has-glass) .response-container pre code {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
            outline: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* ask-container 의 블러·그림자 레이어 제거 */
 | 
			
		||||
        :host-context(body.has-glass) .ask-container::before {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* hover/active 때 다시 생기는 배경도 차단 */
 | 
			
		||||
        :host-context(body.has-glass) .copy-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .close-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .line-copy-button,
 | 
			
		||||
        :host-context(body.has-glass) .line-copy-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .response-line:hover {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* 스크롤바 트랙·썸 마저 투명화 (원할 경우) */
 | 
			
		||||
        :host-context(body.has-glass) .response-container::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) .response-container::-webkit-scrollbar-thumb {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
@ -1378,9 +1415,9 @@ export class AskView extends LitElement {
 | 
			
		||||
            const responseHeight = responseEl.scrollHeight;
 | 
			
		||||
            const inputHeight    = (inputEl && !inputEl.classList.contains('hidden')) ? inputEl.offsetHeight : 0;
 | 
			
		||||
 | 
			
		||||
            const idealHeight = headerHeight + responseHeight + inputHeight + 20; // padding
 | 
			
		||||
            const idealHeight = headerHeight + responseHeight + inputHeight;
 | 
			
		||||
 | 
			
		||||
            const targetHeight = Math.min(700, Math.max(200, idealHeight));
 | 
			
		||||
            const targetHeight = Math.min(700, idealHeight);
 | 
			
		||||
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.invoke('adjust-window-height', targetHeight);
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ export class CustomizeView extends LitElement {
 | 
			
		||||
        :host {
 | 
			
		||||
            display: block;
 | 
			
		||||
            width: 180px;
 | 
			
		||||
            min-height: 180px;
 | 
			
		||||
            height: 100%;
 | 
			
		||||
            color: white;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -234,6 +234,38 @@ export class CustomizeView extends LitElement {
 | 
			
		||||
            font-size: 11px;
 | 
			
		||||
            margin-bottom: 4px;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) .settings-container,
 | 
			
		||||
        :host-context(body.has-glass) .settings-button,
 | 
			
		||||
        :host-context(body.has-glass) .cmd-key,
 | 
			
		||||
        :host-context(body.has-glass) .shortcut-key,
 | 
			
		||||
        :host-context(body.has-glass) .api-key-section input {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
            outline: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* 블러·그림자·gradient 레이어 제거 */
 | 
			
		||||
        :host-context(body.has-glass) .settings-container::before {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* hover/active 시 다시 생기는 배경도 차단 */
 | 
			
		||||
        :host-context(body.has-glass) .settings-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .shortcut-item:hover,
 | 
			
		||||
        :host-context(body.has-glass) .settings-button.danger:hover {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border-color: transparent !important;
 | 
			
		||||
            transform: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* 스크롤바 트랙·썸 투명화(선택 사항) */
 | 
			
		||||
        :host-context(body.has-glass) .settings-container::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) .settings-container::-webkit-scrollbar-thumb {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
@ -390,9 +422,7 @@ export class CustomizeView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    updateScrollHeight() {
 | 
			
		||||
        const windowHeight = window.innerHeight;
 | 
			
		||||
        const headerHeight = 60;
 | 
			
		||||
        const padding = 40;
 | 
			
		||||
        const maxHeight = windowHeight - headerHeight - padding;
 | 
			
		||||
        const maxHeight = windowHeight;
 | 
			
		||||
        
 | 
			
		||||
        this.style.maxHeight = `${maxHeight}px`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -161,7 +161,7 @@ export class AssistantView extends LitElement {
 | 
			
		||||
            /* outline: 0.5px rgba(255, 255, 255, 0.5) solid; */
 | 
			
		||||
            /* outline-offset: -1px; */
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            min-height: 200px;
 | 
			
		||||
            height: 100%;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .assistant-container::after {
 | 
			
		||||
@ -542,6 +542,73 @@ export class AssistantView extends LitElement {
 | 
			
		||||
            font-size: 10px;
 | 
			
		||||
            color: rgba(255, 255, 255, 0.7);
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container,
 | 
			
		||||
        :host-context(body.has-glass) .top-bar,
 | 
			
		||||
        :host-context(body.has-glass) .toggle-button,
 | 
			
		||||
        :host-context(body.has-glass) .copy-button,
 | 
			
		||||
        :host-context(body.has-glass) .transcription-container,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container,
 | 
			
		||||
        :host-context(body.has-glass) .stt-message,
 | 
			
		||||
        :host-context(body.has-glass) .outline-item,
 | 
			
		||||
        :host-context(body.has-glass) .request-item,
 | 
			
		||||
        :host-context(body.has-glass) .markdown-content,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container pre,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container p code,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container pre code {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
            outline: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* 가상 레이어·gradient 테두리 제거 */
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container::before,
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container::after {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* hover 상태에서 생기는 배경도 차단 */
 | 
			
		||||
        :host-context(body.has-glass) .toggle-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .copy-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .outline-item:hover,
 | 
			
		||||
        :host-context(body.has-glass) .request-item.clickable:hover,
 | 
			
		||||
        :host-context(body.has-glass) .markdown-content:hover {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            transform: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* 스크롤바 트랙·썸도 투명화(선택) */
 | 
			
		||||
        :host-context(body.has-glass) .transcription-container::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) .transcription-container::-webkit-scrollbar-thumb,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container::-webkit-scrollbar-thumb {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) * {
 | 
			
		||||
            animation: none !important;
 | 
			
		||||
            transition: none !important;
 | 
			
		||||
            transform: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* 추가: 둥근 모서리와 스크롤바도 평면화하려면 */
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container,
 | 
			
		||||
        :host-context(body.has-glass) .stt-message,
 | 
			
		||||
        :host-context(body.has-glass) .toggle-button,
 | 
			
		||||
        :host-context(body.has-glass) .copy-button {
 | 
			
		||||
            border-radius: 0 !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) ::-webkit-scrollbar,
 | 
			
		||||
        :host-context(body.has-glass) ::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) ::-webkit-scrollbar-thumb {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            width: 0 !important;      /* 스크롤바 자체 숨기기 */
 | 
			
		||||
        }
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    static properties = {
 | 
			
		||||
@ -777,9 +844,9 @@ export class AssistantView extends LitElement {
 | 
			
		||||
 | 
			
		||||
                const contentHeight = activeContent.scrollHeight;
 | 
			
		||||
 | 
			
		||||
                const idealHeight = topBarHeight + contentHeight + 20;
 | 
			
		||||
                const idealHeight = topBarHeight + contentHeight;
 | 
			
		||||
 | 
			
		||||
                const targetHeight = Math.min(700, Math.max(200, idealHeight));
 | 
			
		||||
                const targetHeight = Math.min(700, idealHeight);
 | 
			
		||||
 | 
			
		||||
                console.log(
 | 
			
		||||
                    `[Height Adjusted] Mode: ${this.viewMode}, TopBar: ${topBarHeight}px, Content: ${contentHeight}px, Ideal: ${idealHeight}px, Target: ${targetHeight}px`
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user