From c2d457ed844fe15c73cabef61da92eff74c93162 Mon Sep 17 00:00:00 2001 From: toorux Date: Fri, 9 Oct 2026 18:19:42 +0800 Subject: [PATCH] Add mobile manager navigation and touch-first page sheets --- ui/locales/en-US.json | 16 +++++ ui/locales/zh-CN.json | 16 +++++ ui/src/main.tsx | 8 ++- ui/src/manager-switcher.tsx | 3 +- ui/src/mobile-manager.css | 116 ++++++++++++++++++++++++++++++++++++ ui/src/mobile-manager.tsx | 23 +++++++ 6 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 ui/src/mobile-manager.css create mode 100644 ui/src/mobile-manager.tsx diff --git a/ui/locales/en-US.json b/ui/locales/en-US.json index dd6e91b..c0cd1c3 100644 --- a/ui/locales/en-US.json +++ b/ui/locales/en-US.json @@ -1014,6 +1014,22 @@ "确认后会替换同名文件,不删除目录中的其他文件。": "Confirmation replaces matching files without deleting other files in the directory.", "已上传 {0} / {1} 个文件": "Uploaded {0} / {1} files", "暂无上传任务": "No uploads yet", + "选择页面": "Choose a page", + "关闭文件导航": "Close folder navigation", + "浏览文件夹": "Browse folders", + "新建与上传": "Create and upload", + "筛选与显示": "Filters and view", + "取消选择": "Cancel selection", + "终端快捷键": "Terminal shortcuts", + "请先选择要复制的终端文本。": "Select terminal text before copying.", + "粘贴到终端": "Paste into terminal", + "粘贴文本": "Paste text", + "在这里使用手机的粘贴功能": "Use your phone’s paste action here", + "发送到终端": "Send to terminal", + "向上": "Up", + "向下": "Down", + "向左": "Left", + "向右": "Right", "上传任务队列": "Upload queue", "同时上传文件数": "Simultaneous file uploads", "清除已结束记录": "Clear finished tasks", diff --git a/ui/locales/zh-CN.json b/ui/locales/zh-CN.json index fc0385c..64e694e 100644 --- a/ui/locales/zh-CN.json +++ b/ui/locales/zh-CN.json @@ -1014,6 +1014,22 @@ "确认后会替换同名文件,不删除目录中的其他文件。": "确认后会替换同名文件,不删除目录中的其他文件。", "已上传 {0} / {1} 个文件": "已上传 {0} / {1} 个文件", "暂无上传任务": "暂无上传任务", + "选择页面": "选择页面", + "关闭文件导航": "关闭文件导航", + "浏览文件夹": "浏览文件夹", + "新建与上传": "新建与上传", + "筛选与显示": "筛选与显示", + "取消选择": "取消选择", + "终端快捷键": "终端快捷键", + "请先选择要复制的终端文本。": "请先选择要复制的终端文本。", + "粘贴到终端": "粘贴到终端", + "粘贴文本": "粘贴文本", + "在这里使用手机的粘贴功能": "在这里使用手机的粘贴功能", + "发送到终端": "发送到终端", + "向上": "向上", + "向下": "向下", + "向左": "向左", + "向右": "向右", "上传任务队列": "上传任务队列", "同时上传文件数": "同时上传文件数", "清除已结束记录": "清除已结束记录", diff --git a/ui/src/main.tsx b/ui/src/main.tsx index a1b8cd8..5768122 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -1,3 +1,4 @@ +import {MobilePagePicker,managerPages,useMobileManager,useMobileViewport} from './mobile-manager'; import {FileManager} from './file-manager'; import {TerminalManager} from './terminal'; import {FrameFilePicker} from './file-picker'; @@ -22,7 +23,7 @@ import {InstallReview} from './install-review'; import {MemoryLimit} from './memory-limit'; import {installScrollbars} from '../../sdk/src/scrollbars'; installScrollbars(); -import React,{useEffect,useRef,useState} from 'react';import{createRoot}from'react-dom/client';import{uploadChunk,api,viewKey,exportDiagnosticLogs}from'./api';import {Icon,Brand} from './icons';import {Store,Sources,PluginImage,PluginLinks,PluginAuthor,installedIcon,runJob,Transfer,Job,Presentation,compareVersions} from './store';import {SystemUpdates} from './updates';import './style.css';if(location.pathname==='/launcher')document.documentElement.classList.add('launcher-view');if(location.pathname==='/notifications')document.documentElement.classList.add('notification-view'); +import React,{useEffect,useRef,useState} from 'react';import{createRoot}from'react-dom/client';import{uploadChunk,api,viewKey,exportDiagnosticLogs}from'./api';import {Icon,Brand} from './icons';import {Store,Sources,PluginImage,PluginLinks,PluginAuthor,installedIcon,runJob,Transfer,Job,Presentation,compareVersions} from './store';import {SystemUpdates} from './updates';import './style.css';import './mobile-manager.css';if(location.pathname==='/launcher')document.documentElement.classList.add('launcher-view');if(location.pathname==='/notifications')document.documentElement.classList.add('notification-view'); export type Manifest=Presentation&{id:string;name:string;version:string;author:string;description:string;backend?:{runAs:string;autostart:boolean;memoryLimitMiB?:number};lifecycle?:{runAs?:string};dependencies?:Record;optionalDependencies?:Record;conflicts?:Record;exclusiveResources?:string[];engines?:{framely?:string};ui:{quickPage?:string;windows:Record;launch?:Record;launcherActions?:{id:string;label:string;target:{type:string;entry?:string;window?:string}}[]}}; export type Plugin={manifest:Manifest;enabled:boolean;favorite:boolean;order:number;source?:string;error?:string}; type Source={id:string;name:string;url:string;enabled:boolean;allowHttp:boolean}; @@ -31,6 +32,7 @@ const managerMode=location.pathname==='/manager'; const empty:Status={version:'0.4.0',database:{plugins:{},sources:[],safeMode:false},notifications:[],running:[]}; function App(){ + const mobile=useMobileManager();useMobileViewport(managerMode&&mobile); const[catalogTarget,setCatalogTarget]=useState<{plugin:string;source?:string}|null>(null);const runtimeLabels:Record={starting:t("初始化中"),running:t("运行中"),stopping:t("停止中"),stopped:t("已停止"),recovering:t("等待恢复"),failed:t("失败"),crashed:t("异常退出"),ready:t("已就绪"),installed:t("已安装"),'waiting-dependency':t("等待依赖")};const [status,setStatus]=useState(empty),[error,setError]=useState(''),[tab,setTab]=useState(managerMode?managerTab(location.hash):'favorites'),[page,setPage]=useState(!managerMode&&location.hash.startsWith('#plugin:')?location.hash.slice(8):null),[busy,setBusy]=useState(false),[query,setQuery]=useState(''),[manage,setManage]=useState(null),[install,setInstall]=useState(null),[confirmRunAs,setConfirmRunAs]=useState(false),[logs,setLogs]=useState(null),[importing,setImporting]=useState(false),[transfer,setTransfer]=useState(null),[downloadRequest,setDownloadRequest]=useState(null),[relationAction,setRelationAction]=useState(null),[reviewAgreement,setReviewAgreement]=useState(false),[agreementDeclined,setAgreementDeclined]=useState(false),[languageFiles,setLanguageFiles]=useState([]),[logExport,setLogExport]=useState(''),[exportingLogs,setExportingLogs]=useState(false); useEffect(()=>{const ticket=install?.ticket;return()=>{if(ticket)void api('job.cancel',{job:ticket}).catch(()=>{});};},[install?.ticket]); const localePacks=useRef(null);const localePreference=useRef(null); @@ -89,8 +91,8 @@ function App(){ if(parts[1]==='launcher')return ; if(parts[1]==='notifications')return ; if(windowPlugin){const p=status.database.plugins[windowPlugin];return
{p?.enabled&&!status.database.safeMode?:

{t("插件已停用或正在启动。")}

}{error&&

{error}

}
;} - return
{managerMode?<>{managerSection==='plugins'&&}:<>}
{error&&

{error}

}{status.database.safeMode&&

{t("安全模式:第三方插件暂停运行。")}

} - {selected?<>
{selected.manifest.name}{managerMode&&}
{selected.enabled&&!status.database.safeMode&&selected.manifest.ui.quickPage?:

{t("插件已停用、处于安全模式或没有快捷页面。")}

}:
+ return
{managerMode?<>{managerSection==='plugins'&&}{mobile&&key==='catalog'?void loadCatalog():(setPage(null),setTab(key),history.replaceState(null,'',location.pathname+location.search+'#'+key))}/> }:<>}
{error&&

{error}

}{status.database.safeMode&&

{t("安全模式:第三方插件暂停运行。")}

} + {selected?<>
{selected.manifest.name}{managerMode&&}
{selected.enabled&&!status.database.safeMode&&selected.manifest.ui.quickPage?:

{t("插件已停用、处于安全模式或没有快捷页面。")}

}:
{(tab==='favorites'||tab==='installed')&&<> {managerMode&&

{t("已安装")}

{plugins.length}{t("个插件,")}{plugins.filter(p=>p.enabled).length}{t("个已启用")}

setQuery(e.target.value)}/>
} {managerMode&&
{t("插件")}{t("状态")}{t("版本")}{t("操作")}
}
{listedPlugins.map(p=>
diff --git a/ui/src/manager-switcher.tsx b/ui/src/manager-switcher.tsx index c000aff..7812b4e 100644 --- a/ui/src/manager-switcher.tsx +++ b/ui/src/manager-switcher.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import {IconPuzzle,IconBrandAndroid,IconTerminal2,IconFolder,IconSettings} from '@tabler/icons-react'; import {t} from './i18n'; export type ManagerSection='plugins'|'apk'|'terminal'|'files'|'settings'; export function sectionFor(tab:string):ManagerSection { @@ -9,6 +10,6 @@ export function sectionFor(tab:string):ManagerSection { export function managerTab(hash:string){const key=hash.slice(1);return ['installed','catalog','sources','notification-settings','settings','launcher-settings','about','updates','terminal','files','apk','apk-containers','apk-cleanup','apk-settings'].includes(key)?key:'installed';} export function ManagerSwitcher({section,onChange}:{section:ManagerSection;onChange:(section:ManagerSection)=>void}) { return
- {([['plugins','插件'],['apk','APK'],['terminal','终端'],['files','文件'],['settings','设置']] as const).map(([key,label])=>)} + {([['plugins','插件'],['apk','APK'],['terminal','终端'],['files','文件'],['settings','设置']] as const).map(([key,label])=>)}
; } diff --git a/ui/src/mobile-manager.css b/ui/src/mobile-manager.css new file mode 100644 index 0000000..b089ada --- /dev/null +++ b/ui/src/mobile-manager.css @@ -0,0 +1,116 @@ +/* Mobile product navigation: app chrome 30, contextual drawers 70, sheets 80. + Preserve the existing neutral theme. 12px controls, 20px sheet tops, pill nav items. */ +.manager-section-icon,.mobile-page-picker,.file-mobile-tools,.terminal-touch-keys{display:none} +.mobile-sheet-backdrop{position:absolute;inset:0;z-index:80;background:#09090b99;display:flex;align-items:flex-end} +.mobile-sheet{display:flex;flex-direction:column;width:100%;max-height:90%;background:#242426;border:1px solid #ffffff20;border-bottom:0;border-radius:20px 20px 0 0;padding-bottom:env(safe-area-inset-bottom,0px);overflow:hidden;color:#ededf0} +.mobile-sheet>header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;flex:none;border-bottom:1px solid #ffffff10} +.mobile-sheet>header h2{font-size:18px;font-weight:600;margin:0} +.mobile-sheet>header button{background:transparent;border:0;min-width:44px;min-height:44px;padding:8px;color:#b2b2ba} +.mobile-sheet-body{overflow:auto;overscroll-behavior:contain;padding:12px 16px 20px;min-height:0} +.mobile-page-options{display:grid;gap:6px} +.mobile-page-options button{min-height:52px;background:transparent;border:0;display:flex;justify-content:space-between;padding:12px 14px;border-radius:12px;color:#ededf0;text-align:left;font-size:16px} +.mobile-page-options button[aria-current]{background:#3a3a3e} +@media(max-width:767px){ + html,body,#root{height:100%;height:100dvh;overflow:hidden} + .app.manager{height:var(--manager-mobile-height,100dvh);border-radius:0;clip-path:none;padding-top:env(safe-area-inset-top,0px);padding-bottom:calc(68px + env(safe-area-inset-bottom,0px))} + .manager .shell-header{min-height:58px;height:auto;padding:8px 14px;gap:8px;flex-wrap:nowrap;position:relative;z-index:30;background:var(--manager-chrome-tint)} + .manager .shell-header .brand-title{flex:none;margin-right:auto} + .manager .brand-wordmark{width:110px;height:28px} + .manager .shell-header>.icon-button{display:none} + .manager .shell-header .manager-install-button{font-size:0;min-height:44px;width:44px;padding:10px;border-radius:12px;order:3} + .manager .shell-header .manager-install-button svg{width:22px;height:22px} + .manager .shell-header .manager-switcher{position:fixed;z-index:30;left:0;right:0;bottom:0;width:100%;max-width:none;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));border:0;border-top:1px solid #ffffff18;border-radius:0;background:#202022f5;gap:0;padding:6px 6px calc(6px + env(safe-area-inset-bottom,0px));overflow:visible} + .manager .manager-switcher button{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:56px;padding:5px 2px;gap:3px;font-size:11px;border-radius:12px;color:#b2b2ba} + .manager .manager-switcher button.active{background:transparent;color:#a2c7df} + .manager-section-icon{display:flex;padding:3px 12px;border-radius:999px} + .manager-switcher button.active .manager-section-icon{background:#a2c7df18} + .mobile-page-picker{display:block;min-width:0;max-width:55%} + .mobile-page-picker>button{border:0;border-radius:12px;background:#ffffff08;color:var(--text);font-size:14px;min-height:42px;padding:8px 10px;gap:7px;white-space:nowrap} + .mobile-page-picker>button:disabled{opacity:1} + .mobile-page-picker .mobile-sheet-backdrop{position:fixed} + .manager .workspace{display:flex;min-width:0;min-height:0} + .manager .workspace>nav{display:none} + .manager .content{padding:18px 16px 24px;min-width:0;overscroll-behavior:contain} + .manager h1{font-size:24px;letter-spacing:-.4px} + .manager h2{font-size:19px} + .manager p,.manager .setting p,.manager .store-copy p{font-size:14px;line-height:1.55} + .manager button{touch-action:manipulation} + .manager .content button{min-height:44px;font-size:14px} + .manager input:not([type=checkbox]):not([type=file]),.manager textarea{font-size:16px!important;min-width:0;max-width:100%} + .manager .list-heading{gap:14px;margin-bottom:18px;align-items:flex-start} + .manager .list-heading>div{min-width:0} + .manager .list-heading .search-field,.manager .search-field{min-width:0;width:100%} + .manager .list-heading p{font-size:14px} + .manager .setting{padding:18px 0;margin:0;width:100%} + .manager .setting h2{font-size:18px} + .manager .setting label{font-size:14px} + .manager .switch-row{gap:14px;width:100%;min-width:0} + .manager .switch-row b{font-size:15px} + .manager .switch-row small{font-size:13px;line-height:1.5} + .manager .ui-switch{flex:none;min-width:50px!important;min-height:30px!important} + .manager .plugin-row{padding:16px 0;gap:12px;align-items:flex-start;flex-wrap:wrap} + .manager .plugin-copy{min-width:0;flex:1} + .manager .plugin-copy h2{font-size:17px} + .manager .plugin-copy small{font-size:12px} + .manager .plugin-copy p{font-size:14px} + .manager .plugin-actions{width:100%;display:flex;justify-content:flex-end} + .manager .source-row{padding:16px 0;gap:12px} + .manager .store-filters{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px} + .manager .store-filters .search-field{grid-column:1/-1;min-width:0} + .manager .store-filters .framely-select{min-width:0;max-width:100%} + .manager .store-card{min-width:0} + .manager .store-copy h2{font-size:18px} + .manager .store-card>.plugin-avatar{width:52px!important;height:52px!important} + .manager .detail-heading{gap:12px} + .manager .modal{position:absolute;left:0!important;right:0!important;bottom:0!important;top:auto!important;transform:none!important;width:100%;max-width:none;max-height:92%;border-radius:20px 20px 0 0;padding:20px 18px calc(20px + env(safe-area-inset-bottom,0px));box-shadow:0 0 0 2000px #09090b99;border:1px solid #ffffff20;z-index:80;overflow:auto} + .manager .modal h2{font-size:21px} + .manager .modal footer{display:flex;gap:10px;position:sticky;bottom:0;background:#242426;padding-top:14px} + .manager .modal footer button{flex:1;min-height:46px;font-size:15px} + .manager .modal.install-review{padding:0} + .manager .install-review-body{padding:16px 18px} + .manager .install-review-heading{padding:20px 18px} + .manager .install-review-footer{padding:14px 18px;flex-direction:column} + .manager .install-review-actions{display:grid;grid-template-columns:1fr 1fr;width:100%} + .manager .install-review .install-review-actions .primary{grid-row:auto} + .manager .app>.error,.manager .app>.banner{margin:8px 14px;font-size:13px} + .manager-keyboard-open .manager .shell-header .manager-switcher{display:none} + .manager-keyboard-open .app.manager{padding-bottom:0} + .manager .apk-row{display:grid;grid-template-columns:52px minmax(0,1fr);gap:12px;padding:16px 0} + .manager .apk-row>.plugin-avatar{width:52px!important;height:52px!important} + .manager .apk-row .apk-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr;width:100%} + .manager .apk-copy{min-width:0} + .manager .apk-copy h2{font-size:17px;display:flex;flex-wrap:wrap;gap:6px} + .manager .apk-copy code,.manager .apk-container code{font-size:12px;overflow-wrap:anywhere} + .manager .apk-copy p{font-size:13px} + .manager .apk-container{display:flex;flex-direction:column;gap:12px;padding:16px 0} + .manager .apk-actions{flex-wrap:wrap;gap:8px} + .manager .apk-actions button{flex:1;min-width:0;white-space:normal} + .manager .apk-filters{display:grid;grid-template-columns:1fr;gap:10px} + .manager .apk-filters>*{width:100%;min-width:0} + .manager .apk-cleanup-row{gap:12px;flex-wrap:wrap} + .manager .notification-plugin-row,.manager .notification-plugin-columns{grid-template-columns:minmax(0,1fr) 50px 50px} + .manager .notification-plugin-name b{font-size:14px} + .manager .notification-option p{font-size:13px} +} +@media(max-width:767px) and (prefers-reduced-motion:no-preference){.mobile-sheet{animation:manager-sheet-in .18s ease-out} @keyframes manager-sheet-in{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}} +@media(max-width:767px){ + .manager .plugin-row{display:grid;grid-template-columns:minmax(0,1fr) 44px 44px;gap:8px} + .manager .plugin-main{gap:12px;padding:4px 0;grid-column:1/-1;min-height:62px;width:100%} + .manager .plugin-copy b{font-size:17px;white-space:normal} + .manager .plugin-copy small{font-size:13px} + .manager .plugin-meta{display:flex;font-size:12px} + .manager .plugin-switch{display:block;grid-column:1;justify-self:start;padding:0!important} + .manager .plugin-switch.ui-switch{width:50px;height:30px} + .manager .plugin-row>.icon-button{min-width:44px;min-height:44px;padding:10px} + .manager .plugin-row .row-state,.manager .plugin-row .row-version,.manager .list-columns{display:none} + .manager .proxy-fields,.manager .manage-grid{grid-template-columns:minmax(0,1fr)} + .manager .framely-select button[aria-haspopup]{white-space:normal;overflow-wrap:anywhere} + .manager .framely-select [role=listbox]{background:#242426!important;border-color:#55555c!important;max-width:calc(100vw - 32px)!important} + .manager .framely-select [role=option]{min-height:44px!important;color:#ededf0} + .manager .framely-select [aria-selected=true]{background:#3a3a3e!important} +} +@media(max-width:767px){.manager .file-mobile-tools{display:flex}.manager .file-select-mode .file-status-bar>span{visibility:hidden}} +@media(max-width:767px){ + .manager .apk-manager>.list-heading>.apk-actions{width:100%;display:grid;grid-template-columns:1fr 1fr} + .manager .apk-manager>.list-heading>.apk-actions button{white-space:nowrap;min-height:46px;padding:10px 12px} +} diff --git a/ui/src/mobile-manager.tsx b/ui/src/mobile-manager.tsx new file mode 100644 index 0000000..93c8858 --- /dev/null +++ b/ui/src/mobile-manager.tsx @@ -0,0 +1,23 @@ +import React,{useEffect,useRef,useState} from 'react'; +import {IconChevronDown,IconCheck,IconX} from '@tabler/icons-react'; +import {t} from './i18n'; +import type {ManagerSection} from './manager-switcher'; +export function useMobileManager(){ + const [mobile,setMobile]=useState(()=>matchMedia('(max-width: 767px)').matches); + useEffect(()=>{const query=matchMedia('(max-width: 767px)'),update=()=>setMobile(query.matches);query.addEventListener('change',update);return()=>query.removeEventListener('change',update);},[]);return mobile; +} +export function MobileSheet({title,onClose,children}:{title:string;onClose:()=>void;children:React.ReactNode}){ + const ref=useRef(null),closeRef=useRef(onClose);closeRef.current=onClose; + useEffect(()=>{const previous=document.activeElement as HTMLElement|null;const items=()=>Array.from(ref.current?.querySelectorAll('button:not(:disabled),input:not(:disabled),textarea,[tabindex="0"]')??[]).filter(el=>el.getClientRects().length);items()[0]?.focus(); + const key=(e:KeyboardEvent)=>{if(e.key==='Escape'){if((e.target as HTMLElement)?.closest('.framely-select')?.querySelector('[aria-expanded="true"]'))return;e.preventDefault();e.stopPropagation();closeRef.current();}if(e.key==='Tab'){const list=items(),first=list[0],last=list.at(-1);if(e.shiftKey&&document.activeElement===first){e.preventDefault();last?.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first?.focus();}}};document.addEventListener('keydown',key,true);return()=>{document.removeEventListener('keydown',key,true);if(previous?.isConnected&&!document.querySelector('.modal[aria-modal="true"]'))previous.focus();};},[]); + return
{if(e.target===e.currentTarget)onClose();}}>

{title}

{children}
; +} +export const managerPages=(section:ManagerSection):[string,string,string][]=>section==='settings'?[['settings',t('通用'),'settings'],['launcher-settings',t('启动台'),'launcher'],['updates',t('更新'),'refresh'],['about',t('关于'),'info']]:section==='apk'?[['apk',t('应用'),'download'],['apk-containers',t('容器'),'plugins'],['apk-cleanup',t('数据清理'),'settings'],['apk-settings',t('设置'),'settings']]:section==='plugins'?[['installed',t('已安装'),'plugins'],['catalog',t('插件库'),'download'],['sources',t('插件源导航'),'link'],['notification-settings',t('通知设置导航'),'bell']]:[]; +export function MobilePagePicker({section,tab,onChange}:{section:ManagerSection;tab:string;onChange:(tab:string)=>void}){ + const [open,setOpen]=useState(false),pages=managerPages(section),label=pages.find(p=>p[0]===tab)?.[1]??t(section==='terminal'?'终端':'文件'); + useEffect(()=>setOpen(false),[section,tab]); + return
{open&&setOpen(false)}>
{pages.map(([key,label])=>)}
}
; +} +export function useMobileViewport(enabled:boolean){ + useEffect(()=>{if(!enabled)return;const viewport=window.visualViewport;const update=()=>{const keyboard=window.innerHeight-(viewport?.height??window.innerHeight)>140;document.documentElement.style.setProperty('--manager-mobile-height',`${viewport?.height??window.innerHeight}px`);document.documentElement.classList.toggle('manager-keyboard-open',keyboard);};update();viewport?.addEventListener('resize',update);window.addEventListener('resize',update);return()=>{viewport?.removeEventListener('resize',update);window.removeEventListener('resize',update);document.documentElement.style.removeProperty('--manager-mobile-height');document.documentElement.classList.remove('manager-keyboard-open');};},[enabled]); +}