@@ -135,6 +135,11 @@
. drawer-head h2 { margin : 0 0 5 px ; font-size : 21 px ; letter-spacing : -.02 em ; }
. drawer-head p { margin : 0 ; color : var ( - - muted ) ; font-size : 13 px ; }
. drawer-body { padding : 22 px 26 px ; display : grid ; gap : 22 px ; }
. drawer-body . instance-form-workspace { margin : 14 px ; padding : 14 px ; gap : 14 px ; border : 1 px solid var ( - - line - strong ) ; border-radius : calc ( var ( - - radius - card ) + 2 px ) ; background : var ( - - surface -2 ) ; }
. instance-form-workspace . form-section { padding : 13 px ; border : 1 px solid var ( - - line ) ; border-radius : 10 px ; background : var ( - - surface ) ; }
. instance-form-workspace . field { padding : 10 px ; border : 1 px solid var ( - - line ) ; border-radius : 9 px ; background : var ( - - surface -2 ) ; }
. instance-form-workspace . field : focus-within { border-color : var ( - - accent ) ; box-shadow : var ( - - focus ) ; }
. instance-form-workspace . drawer-actions { padding : 12 px ; border : 1 px solid var ( - - line ) ; border-radius : 10 px ; background : var ( - - surface ) ; }
. form-section { display : grid ; gap : 13 px ; }
. form-section h3 { margin : 0 ; font-size : 13 px ; color : var ( - - muted ) ; text-transform : uppercase ; letter-spacing : .06 em ; }
. field { display : grid ; gap : 6 px ; }
@@ -155,7 +160,7 @@
. activity-item { display : grid ; grid-template-columns : 9 px 1 fr ; gap : 9 px ; font-size : 12 px ; color : var ( - - muted ) ; }
. activity-item i { width : 8 px ; height : 8 px ; background : var ( - - accent ) ; border-radius : 50 % ; margin-top : 4 px ; }
dialog { border : 0 ; border-radius : var ( - - radius - dialog ) ; padding : 0 ; width : min ( 430 px , calc ( 100 vw - 32 px ) ) ; color : var ( - - text ) ; background : var ( - - surface ) ; box-shadow : var ( - - shadow ) ; }
dialog . edit-dialog { width : min ( 620 px , calc ( 100 vw - 32 px ) ) ; }
dialog . edit-dialog { width : min ( 620 px , calc ( 100 vw - 32 px ) ) ; padding : 10 px ; border : 1 px solid var ( - - line - strong ) ; background : var ( - - surface -2 ) ; }
dialog :: backdrop { background : rgba ( 16 , 24 , 40 , .42 ) ; }
. dialog-inner { padding : 24 px ; display : grid ; gap : 17 px ; }
. dialog-inner h2 { margin : 0 ; font-size : 19 px ; }
@@ -164,7 +169,11 @@
. dialog-list { margin : -3 px 0 0 ; padding-left : 20 px ; color : var ( - - text ) ; display : grid ; gap : 5 px ; }
. dialog-list li { line-height : 1.45 ; }
. dialog-actions { display : flex ; justify-content : flex-end ; gap : 8 px ; }
. edit-form { display : grid ; gap : 18 px ; }
. edit-form { display : grid ; gap : 12 px ; border : 1 px solid var ( - - line ) ; border-radius : var ( - - radius - card ) ; background : var ( - - surface ) ; }
. edit-form > . field { padding : 10 px ; border : 1 px solid var ( - - line ) ; border-radius : 9 px ; background : var ( - - surface -2 ) ; }
. edit-form > . field : focus-within { border-color : var ( - - accent ) ; box-shadow : var ( - - focus ) ; }
. edit-form . form-note { border : 1 px solid color-mix ( in srgb , var ( - - warning ) 35 % , var ( - - line ) ) ; }
. edit-form . dialog-actions { padding : 12 px ; border : 1 px solid var ( - - line ) ; border-radius : 10 px ; background : var ( - - surface -2 ) ; }
. edit-form . path-field { grid-template-columns : minmax ( 0 , 1 fr ) auto ; }
. readonly-value { background : var ( - - surface -3 ) !important ; color : var ( - - muted ) !important ; cursor : default ; }
. form-note { padding : 10 px 12 px ; border-radius : 9 px ; background : var ( - - warning - soft ) ; color : var ( - - warning ) ; font-size : 12 px ; line-height : 1.5 ; }
@@ -359,7 +368,7 @@
if ( $ ( 'discardDialog' ) . open ) $ ( 'discardDialog' ) . close ( ) ; renderRows ( ) ; const savedName = x . name ; finishEdit ( ) ; showToast ( ` 已保存“ ${ savedName } ”的实例配置。 ` ) ;
}
function requestEditClose ( ) { if ( editIsDirty ( ) ) { $ ( 'editDialog' ) . close ( ) ; $ ( 'discardDialog' ) . showModal ( ) ; return ; } finishEdit ( ) ; }
function openLaunch ( ) { $ ( 'drawer' ) . innerHTML = ` <div class="drawer-head"><div><h2>新建浏览器实例</h2><p>配置完成后,Chub 会在本机启动一个隔离环境。</p></div><button class="icon-button" id="closeDrawer" aria-label="关闭新建实例">× </button></div><form class="drawer-body" id="launchForm"><div class="form-section"><h3>浏览器配置</h3><div class="field"><span>浏览器类型 <span>*</span></span><div class="radio-row" role="radiogroup" aria-label="浏览器类型"><label><input type="radio" name="kind" value="chrome" checked> Chrome</label><label><input type="radio" name="kind" value="edge"> Edge</label></div></div><div class="field"><label for="exe">可执行文件</label><input id="exe" value="自动发现" aria-describedby="exeHelp"><small id="exeHelp">留空时按浏览器类型搜索标准安装路径。</small></div></div><div class="form-section"><h3>运行环境</h3><div class="field"><label for="dataDir">User Data Dir <span>*</span></label><div class="path-field"><input id="dataDir" placeholder="例如:D: \\ Chub \\ profiles \\ new-instance" required><button type="button" class="secondary" id="chooseInstanceDir">选择目录</button></div><small>必须是绝对路径;同一目录不能被多个实例同时使用。</small></div><div class="field"><label for="profile">profile-directory</label><input id="profile" value="Default"></div><div class="field"><label for="url">启动 URL(可选)</label><input id="url" type="url" placeholder="https://example.com"><small>仅支持 http/https;留空时启动浏览器默认页。</small></div><div class="field"><label for="launchPreferredPort">首选调试端口</label><input id="launchPreferredPort" type="number" min="1024" max="65535" value="9670" inputmode="numeric"><small>系统从此值开始建议未被其他 Chub 实例预留的端口;实际端口会在启动时验证。</small></div><div class="field"><label for="launchProxy">代理</label><select id="launchProxy"><option value="">无代理</option></select><small>选择设置中保存的无认证代理;端点会在启动时安全传递。</small></div></div><div class="form-section"><h3>启动选项</h3><label class="check-line"><input type="checkbox" id="headless"> <span><b>无头模式</b><br><small class="help">启动后台浏览器,不显示窗口。</small></span></label><label class="check-line"><input type="checkbox" id="remember" checked> <span><b>保存此配置</b><br><small class="help">只保存路径和非敏感参数。</small></span></label></div><div class="drawer-actions"><button type="button" class="secondary" id="cancelLaunch">取消</button><button type="submit" class="primary">启动实例</button></div></form> ` ; showDrawer ( ) ; renderProxyOptions ( $ ( 'launchProxy' ) ) ; $ ( 'closeDrawer' ) . onclick = hideDrawer ; $ ( 'cancelLaunch' ) . onclick = hideDrawer ; $ ( 'chooseInstanceDir' ) . onclick = ( ) => showToast ( '原型演示:将打开目录选择器。' ) ; $ ( 'launchForm' ) . onsubmit = e => { e . preventDefault ( ) ; const data = $ ( 'dataDir' ) , port = Number ( $ ( 'launchPreferredPort' ) . value ) ; if ( ! data . value . trim ( ) ) { data . focus ( ) ; data . parentElement . classList . add ( 'error' ) ; const msg = document . createElement ( 'div' ) ; msg . className = 'field-error' ; msg . textContent = '请填写 User Data Dir。' ; data . parentElement . appendChild ( msg ) ; return ; } if ( ! Number . isInteger ( port ) || port < 1024 || port > 65535 ) { $ ( 'launchPreferredPort' ) . focus ( ) ; showToast ( '请填写有效的首选调试端口。' ) ; return ; } const proxy = proxyLabel ( $ ( 'launchProxy' ) . value ) ; hideDrawer ( ) ; setTimeout ( ( ) => showToast ( ` 启动请求已提交(首选端口 ${ port } ; ${ proxy } ),实例进入 STARTING 状态。 ` ) , 80 ) ; } ; }
function openLaunch ( ) { $ ( 'drawer' ) . innerHTML = ` <div class="drawer-head"><div><h2>新建浏览器实例</h2><p>配置完成后,Chub 会在本机启动一个隔离环境。</p></div><button class="icon-button" id="closeDrawer" aria-label="关闭新建实例">× </button></div><form class="drawer-body instance-form-workspace " id="launchForm"><div class="form-section"><h3>浏览器配置</h3><div class="field"><span>浏览器类型 <span>*</span></span><div class="radio-row" role="radiogroup" aria-label="浏览器类型"><label><input type="radio" name="kind" value="chrome" checked> Chrome</label><label><input type="radio" name="kind" value="edge"> Edge</label></div></div><div class="field"><label for="exe">可执行文件</label><input id="exe" value="自动发现" aria-describedby="exeHelp"><small id="exeHelp">留空时按浏览器类型搜索标准安装路径。</small></div></div><div class="form-section"><h3>运行环境</h3><div class="field"><label for="dataDir">User Data Dir <span>*</span></label><div class="path-field"><input id="dataDir" placeholder="例如:D: \\ Chub \\ profiles \\ new-instance" required><button type="button" class="secondary" id="chooseInstanceDir">选择目录</button></div><small>必须是绝对路径;同一目录不能被多个实例同时使用。</small></div><div class="field"><label for="profile">profile-directory</label><input id="profile" value="Default"></div><div class="field"><label for="url">启动 URL(可选)</label><input id="url" type="url" placeholder="https://example.com"><small>仅支持 http/https;留空时启动浏览器默认页。</small></div><div class="field"><label for="launchPreferredPort">首选调试端口</label><input id="launchPreferredPort" type="number" min="1024" max="65535" value="9670" inputmode="numeric"><small>系统从此值开始建议未被其他 Chub 实例预留的端口;实际端口会在启动时验证。</small></div><div class="field"><label for="launchProxy">代理</label><select id="launchProxy"><option value="">无代理</option></select><small>选择设置中保存的无认证代理;端点会在启动时安全传递。</small></div></div><div class="form-section"><h3>启动选项</h3><label class="check-line"><input type="checkbox" id="headless"> <span><b>无头模式</b><br><small class="help">启动后台浏览器,不显示窗口。</small></span></label><label class="check-line"><input type="checkbox" id="remember" checked> <span><b>保存此配置</b><br><small class="help">只保存路径和非敏感参数。</small></span></label></div><div class="drawer-actions"><button type="button" class="secondary" id="cancelLaunch">取消</button><button type="submit" class="primary">启动实例</button></div></form> ` ; showDrawer ( ) ; renderProxyOptions ( $ ( 'launchProxy' ) ) ; $ ( 'closeDrawer' ) . onclick = hideDrawer ; $ ( 'cancelLaunch' ) . onclick = hideDrawer ; $ ( 'chooseInstanceDir' ) . onclick = ( ) => showToast ( '原型演示:将打开目录选择器。' ) ; $ ( 'launchForm' ) . onsubmit = e => { e . preventDefault ( ) ; const data = $ ( 'dataDir' ) , port = Number ( $ ( 'launchPreferredPort' ) . value ) ; if ( ! data . value . trim ( ) ) { data . focus ( ) ; data . parentElement . classList . add ( 'error' ) ; const msg = document . createElement ( 'div' ) ; msg . className = 'field-error' ; msg . textContent = '请填写 User Data Dir。' ; data . parentElement . appendChild ( msg ) ; return ; } if ( ! Number . isInteger ( port ) || port < 1024 || port > 65535 ) { $ ( 'launchPreferredPort' ) . focus ( ) ; showToast ( '请填写有效的首选调试端口。' ) ; return ; } const proxy = proxyLabel ( $ ( 'launchProxy' ) . value ) ; hideDrawer ( ) ; setTimeout ( ( ) => showToast ( ` 启动请求已提交(首选端口 ${ port } ; ${ proxy } ),实例进入 STARTING 状态。 ` ) , 80 ) ; } ; }
function showDrawer ( ) { $ ( 'drawer' ) . classList . add ( 'open' ) ; $ ( 'backdrop' ) . classList . add ( 'open' ) ; $ ( 'drawer' ) . setAttribute ( 'aria-hidden' , 'false' ) ; setTimeout ( ( ) => $ ( 'drawer' ) . querySelector ( 'button, input, select' ) ? . focus ( ) , 60 ) ; }
function hideDrawer ( ) { $ ( 'drawer' ) . classList . remove ( 'open' ) ; $ ( 'backdrop' ) . classList . remove ( 'open' ) ; $ ( 'drawer' ) . setAttribute ( 'aria-hidden' , 'true' ) ; }
function openConfirm ( id , action ) { const x = instances . find ( i => i . id === id ) ; modalAction = action ; $ ( 'dialogTitle' ) . textContent = action === 'restart' ? '重启浏览器实例?' : action === 'start' ? '启动浏览器实例?' : '关闭浏览器实例?' ; $ ( 'dialogText' ) . textContent = action === 'restart' ? ` 将先请求“ ${ x . name } ”正常退出,再使用相同配置重新启动。 ` : action === 'start' ? ` 将使用已保存配置启动“ ${ x . name } ”。 ` : ` 将请求“ ${ x . name } ”正常退出。未保存的网页内容由浏览器自行处理。 ` ; $ ( 'dialogWarning' ) . hidden = action !== false ; $ ( 'dialogConfirm' ) . textContent = action === false ? '优雅关闭' : action === 'restart' ? '确认重启' : '启动实例' ; $ ( 'dialogConfirm' ) . className = action === false ? 'danger-button' : 'primary' ; $ ( 'confirmDialog' ) . showModal ( ) ; }