Rive 中文术语管理

Rive编辑器自定义中文术语词典,目前支持:词条分组管理/编辑删除/导入导出/搜索/组内拖拽/分组拖拽/蓝色插入线/删空组/面板收起成可拖拽按钮;如果需要支持其他网站,自主进行代码更改,或留言添加

// ==UserScript==
// @name         Rive 中文术语管理
// @namespace    http://tampermonkey.net/
// @version      1.5.2
// @description  Rive编辑器自定义中文术语词典,目前支持:词条分组管理/编辑删除/导入导出/搜索/组内拖拽/分组拖拽/蓝色插入线/删空组/面板收起成可拖拽按钮;如果需要支持其他网站,自主进行代码更改,或留言添加
// @match        https://editor.rive.app/*
// @match        https://app.rive.app/*
// @match        https://rive.app/*
// @match        https://*.rive.app/*
// @run-at       document-start
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  var DEBUG = false;
  function log(){ if (DEBUG) try{ console.log('[Rive-I18N]', ...arguments);}catch(_){} }

  var STORAGE_KEY = 'rive_terms_dict_shadow_v11';
  var UI_STATE_KEY = 'rive_panel_ui_shadow_v11';
  var uidSeed = 1;
  function uid(){ return 'u' + Date.now().toString(36) + (uidSeed++).toString(36); }

  var DEFAULT_DICT = [
    { uid: uid(), group: '动画', items: [{ uid: uid(), key: 'Animation', value: '动画' }, { uid: uid(), key: 'Timeline', value: '时间轴' }] },
    { uid: uid(), group: '状态机', items: [{ uid: uid(), key: 'State Machine', value: '状态机' }, { uid: uid(), key: 'Transition', value: '过渡' }] }
  ];
  function loadDict(){
    try{
      var raw = localStorage.getItem(STORAGE_KEY);
      var d = raw ? JSON.parse(raw) : DEFAULT_DICT;
      d.forEach(function(g){ if(!g.uid) g.uid = uid(); if(!Array.isArray(g.items)) g.items=[]; g.items.forEach(function(it){ if(!it.uid) it.uid=uid(); }); });
      return d;
    }catch(_){ return DEFAULT_DICT; }
  }
  function saveDict(){ localStorage.setItem(STORAGE_KEY, JSON.stringify(dict)); }

  function loadUIState(){
    try{
      var raw = localStorage.getItem(UI_STATE_KEY);
      var s = raw ? JSON.parse(raw) : {};
      return { minimized: !!s.minimized, panelPos: s.panelPos || null, miniPos: s.miniPos || null, collapsed: s.collapsed || {} };
    }catch(_){ return { minimized:false, panelPos:null, miniPos:null, collapsed:{} }; }
  }
  function saveUIState(){ localStorage.setItem(UI_STATE_KEY, JSON.stringify(uiState)); }

  var dict = loadDict();
  var uiState = loadUIState();

  var hostEl=null, shadow=null, root=null, panelEl=null, miniEl=null, groupsEl=null;

  function setFixed(el,x,y){ el.style.left=x+'px'; el.style.top=y+'px'; el.style.right='auto'; }
  function clamp(n,a,b){ return Math.max(a, Math.min(b,n)); }
  function restoreFixed(box,pos){
    if(!box) return;
    var w=box.offsetWidth||0, h=box.offsetHeight||0;
    var vx=window.innerWidth, vy=window.innerHeight;
    var x=(pos&&Number.isFinite(pos.x))?pos.x:(vx-w-20);
    var y=(pos&&Number.isFinite(pos.y))?pos.y:80;
    x=Math.max(0, Math.min(vx-w,x)); y=Math.max(0, Math.min(vy-h,y));
    setFixed(box,x,y);
  }

  var CSS = ''
  + '#rive-root, #rive-panel, #rive-mini{font-family: Inter, ui-sans-serif, system-ui, Arial, sans-serif; line-height: 1.4;}'
  + '#rive-panel{display:block; position:fixed; top:80px; right:20px; width:440px; background:#3D3D3D; color:#FFFFFF; border:1px solid #3A3A3A; border-radius:10px; font-size:13px; box-shadow:0 6px 18px rgba(0,0,0,.6); overflow:hidden; z-index:2147483647;}'
  + '#rive-panel *{box-sizing:border-box;}'
  + '#rive-header{background:#474747;color:#FFFFFF;display:flex;align-items:center;justify-content:space-between;padding:12px;user-select:none;cursor:move;}'
  + '#rive-title{margin-left:12px;font-weight:700;}'
  + '#rive-header .right{display:inline-flex;align-items:center;column-gap:12px;margin-right:12px;}'
  + '#rive-header .btn{border:1px solid #666;background:#5a5a5a;color:#fff;border-radius:6px;padding:2px 10px;cursor:pointer;}'
  + '#rive-header .btn:hover{filter:brightness(1.06);}'
  + '#rive-inner{padding:12px;display:flex;flex-direction:column;gap:12px;background:#3D3D3D;}'
  + '.rive-search-wrap{background:rgba(0,0,0,0.15);border:1px solid #3A3A3A;border-radius:8px;padding:8px;display:flex;align-items:center;gap:8px;}'
  + '.rive-search-wrap .icon{opacity:.65;}'
  + '#rive-search{flex:1;background:transparent;border:none;outline:none;color:#FFFFFFCC;font-size:13px;}'
  + '#rive-search::placeholder{color:#FFFFFF99;opacity:.8;}'
  + '#rive-groups{max-height:560px;overflow:auto;position:relative;border-radius:8px;}'
  + '.rive-group{background:#3D3D3D;border-radius:8px;margin:0 0px 12px 0px;}'
  + '.rive-group:last-child{margin-bottom:4px;}'
  + '.rive-group-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border:1px solid #454545;border-radius:6px;background:#3D3D3D;}'
  + '.rive-group-header .left{display:inline-flex;align-items:center;gap:8px;}'
  + '.rive-toggle{width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;border:1px solid #555;border-radius:4px;background:#2F2F2F;color:#fff;cursor:pointer;}'
  + '.rive-group-title{font-weight:600;color:#FFFFFF;}'
  + '.rive-group-handle{cursor:grab;color:#FFFFFF99;user-select:none;padding:2px 6px;border-radius:4px;}'
  + '.rive-group-handle:hover{background:rgba(255,255,255,.08);}'
  + '.rive-items{padding-top:8px;}'
  + '.rive-row{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:4px 0;padding:8px 10px;cursor:grab;border:1px solid #3a3a3a;border-radius:6px;background:#FFFFFF0D;transition:background .12s ease,border-color .12s ease;}'
  + '.rive-row:first-child{margin-top:0;}.rive-row:last-child{margin-bottom:0;}'
  + '.rive-row:hover{background:#FFFFFF1A;border-color:#4a4a4a;}'
  + '.rive-row .term{font-weight:400;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:70%;}'
  + '.rive-row .actions{display:inline-flex;align-items:center;column-gap:12px;}'
  + '.rive-row .act{color:#7ECBFF;cursor:pointer;font-weight:600;text-decoration:none;}'
  + '.rive-row .act:hover{text-decoration:underline;}'
  + '.dragging{opacity:.85;}'
  + '#rive-insert-indicator{position:absolute;left:8px;right:8px;height:2px;background:#2AA0FF;box-shadow:0 0 0 1px rgba(42,160,255,.25);pointer-events:none;display:none;}'
  + '#rive-footer{display:flex;align-items:center;justify-content:center;gap:12px;background:#3D3D3D;border-radius:8px;padding:8px;}'
  + '.rive-btn{border:1px solid #888;background:#5A5A5A;color:#fff;border-radius:8px;padding:8px 12px;cursor:pointer;}'
  + '.rive-btn:hover{filter:brightness(1.06);}'
  + '#rive-mini{position:fixed;top:80px;right:20px;background:#474747;color:#fff;border:1px solid #3A3A3A;border-radius:20px;padding:8px 12px;cursor:move;user-select:none;box-shadow:0 6px 18px rgba(0,0,0,.4);font-size:13px;font-weight:600;z-index:2147483647;}'
  + '#rive-mini .tap{cursor:pointer;}'
  + '.hidden{display:none !important;}';

  function injectStylesSafe(shadowRoot, cssText){
    try{
      if ('adoptedStyleSheets' in shadowRoot && 'CSSStyleSheet' in window) {
        var sheet = new CSSStyleSheet(); sheet.replaceSync(cssText);
        shadowRoot.adoptedStyleSheets = (shadowRoot.adoptedStyleSheets || []).concat(sheet);
        log('styles: adoptedStyleSheets'); return 'adopted';
      }
    }catch(e1){ log('adopted failed', e1); }
    try{
      var blob = new Blob([cssText], {type:'text/css'});
      var link = document.createElement('link'); link.rel='stylesheet'; link.href=URL.createObjectURL(blob);
      shadowRoot.appendChild(link);
      window.addEventListener('unload', function(){ try{ URL.revokeObjectURL(link.href);}catch(e){} });
      log('styles: blob link'); return 'blob';
    }catch(e2){ log('blob failed', e2); }
    try{
      var style = document.createElement('style'); style.textContent = cssText; shadowRoot.appendChild(style);
      log('styles: inline style'); return 'inline';
    }catch(e3){ log('inline failed', e3); }
    return 'none';
  }

  function applyEmergencyPanelStyle(el){
    if(!el) return;
    el.style.cssText += ';position:fixed;top:80px;right:20px;width:520px;display:block;background:#3D3D3D;color:#fff;border:1px solid #3A3A3A;border-radius:10px;font:13px Inter,ui-sans-serif,system-ui,Arial,sans-serif;box-shadow:0 6px 18px rgba(0,0,0,.6);z-index:2147483647;';
  }
  function applyEmergencyMiniStyle(el){
    if(!el) return;
    el.style.cssText += ';position:fixed;top:80px;right:20px;background:#474747;color:#fff;border:1px solid #3A3A3A;border-radius:20px;padding:8px 12px;font:600 13px Inter,ui-sans-serif,system-ui,Arial,sans-serif;box-shadow:0 6px 18px rgba(0,0,0,.4);z-index:2147483647;cursor:move;';
  }

  function waitForDomReady(cb, tries){
    tries = tries || 0;
    if (document.body || document.documentElement) { cb(); return; }
    if (tries > 300) { cb(); return; }
    requestAnimationFrame(function(){ waitForDomReady(cb, tries+1); });
  }

  function makeDraggable(box, handle, onMove){
    var down=false, dx=0, dy=0;
    handle.addEventListener('mousedown', function(e){
      var t = e.target;
      var isBtn = t && (t.id==='rive-close' || t.id==='rive-collapse' || t.classList.contains('tap'));
      if (isBtn) return;
      down=true; var r=box.getBoundingClientRect(); dx=e.clientX-r.left; dy=e.clientY-r.top;
    });
    document.addEventListener('mouseup', function(){ down=false; });
    document.addEventListener('mousemove', function(e){
      if(!down) return;
      var x = clamp(e.clientX - dx, 0, window.innerWidth - box.offsetWidth);
      var y = clamp(e.clientY - dy, 0, window.innerHeight - box.offsetHeight);
      setFixed(box, x, y); if (typeof onMove==='function') onMove(x,y);
    });
  }

  var DND = { mode:null, dragGroupEl:null, dragItemEl:null, indicator:null, tgtGroupIdx:null, tgtItemIdx:null };

  function onGroupHandleDragStart(ev){ ev.dataTransfer.effectAllowed='move'; DND.mode='group'; DND.dragGroupEl = ev.currentTarget.closest('.rive-group'); }
  function onGroupHandleDragEnd(){ DND.mode=null; DND.dragGroupEl=null; hideIndicator(); }
  function onRowDragStart(e){ DND.mode='item'; DND.dragItemEl=e.currentTarget; e.currentTarget.classList.add('dragging'); e.dataTransfer.effectAllowed='move'; }
  function onRowDragEnd(e){ e.currentTarget.classList.remove('dragging'); DND.mode=null; DND.dragItemEl=null; DND.tgtItemIdx=null; hideIndicator(); }

  function onGroupsClick(e){
    var t=e.target; if(!t) return;
    if (t.classList.contains('rive-toggle')){
      var gid=t.dataset.uid; var curOpen=!uiState.collapsed[gid];
      uiState.collapsed[gid] = curOpen ? true : false; saveUIState();
      renderGroups(shadow.getElementById('rive-search').value.trim().toLowerCase()); return;
    }
    if (t.classList.contains('act')){
      var action=t.dataset.action; var row=t.closest('.rive-row'); var group=t.closest('.rive-group');
      if (!row || !group) return;
      var gUid=group.dataset.uid, itUid=row.dataset.uid;
      var gIndex = dict.findIndex(function(g){ return g.uid===gUid; }); if (gIndex<0) return;
      var iIndex = dict[gIndex].items.findIndex(function(it){ return it.uid===itUid; }); if (iIndex<0) return;

      if (action==='edit'){
        var cur=dict[gIndex].items[iIndex];
        var nk=window.prompt('编辑英文术语', cur.key); if(!nk) return;
        var nv=window.prompt('编辑中文翻译', cur.value); if(nv===null) return;
        dict[gIndex].items[iIndex].key=nk; dict[gIndex].items[iIndex].value=nv; saveDict();
        renderGroups(shadow.getElementById('rive-search').value.trim().toLowerCase());
      } else if (action==='del'){
        if(!window.confirm('确认删除此词条?')) return;
        dict[gIndex].items.splice(iIndex,1);
        if (dict[gIndex].items.length===0) dict.splice(gIndex,1);
        saveDict();
        renderGroups(shadow.getElementById('rive-search').value.trim().toLowerCase());
      }
    }
  }

  function ensureHost(){
    var existed=document.getElementById('rive-i18n-host');
    if (existed){
      hostEl=existed; shadow=hostEl.shadowRoot || hostEl.attachShadow({mode:'open'});
      root=shadow.getElementById('rive-root'); if(!root){ root=document.createElement('div'); root.id='rive-root'; shadow.appendChild(root); }
      return true;
    }
    hostEl=document.createElement('div'); hostEl.id='rive-i18n-host';
    hostEl.style.cssText='position:fixed;left:0;top:0;width:0;height:0;z-index:2147483647;';
    (document.body||document.documentElement).appendChild(hostEl);
    shadow=hostEl.attachShadow({mode:'open'}); root=document.createElement('div'); root.id='rive-root'; shadow.appendChild(root);
    return true;
  }

  function buildPanel(){
    ensureHost(); if(!shadow||!root) return;
    root.innerHTML='';
    injectStylesSafe(shadow, CSS);

    panelEl=document.createElement('div'); panelEl.id='rive-panel';
    panelEl.innerHTML =
      '<div id="rive-header">'+
        '<div id="rive-title">Rive 中文术语管理</div>'+
        '<div class="right">'+
          '<button id="rive-collapse" class="btn" title="收起">收起</button>'+
          '<button id="rive-close" class="btn" title="关闭">✖</button>'+
        '</div>'+
      '</div>'+
      '<div id="rive-inner">'+
        '<div class="rive-search-wrap"><span class="icon">🔍</span><input id="rive-search" type="text" placeholder="搜索英文或中文..."></div>'+
        '<div id="rive-groups"></div>'+
        '<div id="rive-footer">'+
          '<button id="btn-add" class="rive-btn">添加新词条</button>'+
          '<button id="btn-import" class="rive-btn">导入词条 JSON</button>'+
          '<button id="btn-export" class="rive-btn">导出词条 JSON</button>'+
        '</div>'+
      '</div>';
    root.appendChild(panelEl);

    var headerEl=panelEl.querySelector('#rive-header'); groupsEl=panelEl.querySelector('#rive-groups');

    panelEl.querySelector('#rive-close').addEventListener('click', function(){ root.innerHTML=''; panelEl=null; miniEl=null; });
    panelEl.querySelector('#rive-collapse').addEventListener('click', function(){ toMini(); });
    panelEl.querySelector('#rive-search').addEventListener('input', function(){ renderGroups(this.value.trim().toLowerCase()); });
    makeDraggable(panelEl, headerEl, function(x,y){ uiState.panelPos={x:x,y:y}; saveUIState(); });

    setTimeout(function(){ if(uiState.panelPos&&uiState.panelPos.x!=null&&uiState.panelPos.y!=null){ restoreFixed(panelEl, uiState.panelPos); } },0);

    groupsEl.addEventListener('click', onGroupsClick);

    renderGroups('');
    bindFooter();

    setTimeout(function(){
      try{
        var cs=getComputedStyle(panelEl);
        var bad=!cs || cs.position!=='fixed' || cs.backgroundColor==='rgba(0, 0, 0, 0)' || panelEl.offsetWidth===0;
        if (bad) applyEmergencyPanelStyle(panelEl);
      }catch(_){ applyEmergencyPanelStyle(panelEl); }
      if (uiState.minimized) toMini();
    },120);
  }

  function toMini(){
    if(!shadow||!root) return;
    if(panelEl) panelEl.classList.add('hidden');
    if(!miniEl){
      injectStylesSafe(shadow, CSS);
      miniEl=document.createElement('div'); miniEl.id='rive-mini'; miniEl.innerHTML='<span class="tap">Rive中文</span>';
      root.appendChild(miniEl);
      miniEl.querySelector('.tap').addEventListener('click', function(){
        if(panelEl) panelEl.classList.remove('hidden');
        if(miniEl) miniEl.remove(); miniEl=null;
        uiState.minimized=false; saveUIState();
      });
      makeDraggable(miniEl, miniEl, function(x,y){ uiState.miniPos={x:x,y:y}; saveUIState(); });
      setTimeout(function(){
        try{ var cs=getComputedStyle(miniEl); var bad=!cs||cs.position!=='fixed'||miniEl.offsetWidth===0; if(bad) applyEmergencyMiniStyle(miniEl); }
        catch(_){ applyEmergencyMiniStyle(miniEl); }
      },50);
    }
    uiState.minimized=true; saveUIState();
    setTimeout(function(){ restoreFixed(miniEl, uiState.miniPos); },0);
  }

  function ensureIndicator(){
    if (DND.indicator && groupsEl && groupsEl.contains(DND.indicator)) return DND.indicator;
    var line=document.createElement('div'); line.id='rive-insert-indicator'; groupsEl.appendChild(line); DND.indicator=line; return line;
  }
  function showIndicator(y){ var line=ensureIndicator(); line.style.top=y+'px'; line.style.display='block'; }
  function hideIndicator(){ if (DND.indicator) DND.indicator.style.display='none'; }

  function renderGroups(filter){
    groupsEl.innerHTML=''; hideIndicator();
    for (var gi=0; gi<dict.length; gi++){
      var grp=dict[gi];
      var card=document.createElement('div'); card.className='rive-group'; card.dataset.uid=grp.uid;

      var gh=document.createElement('div'); gh.className='rive-group-header';
      var left=document.createElement('div'); left.className='left';

      var open=!uiState.collapsed[grp.uid];
      var btn=document.createElement('button'); btn.className='rive-toggle'; btn.textContent=open?'▾':'▸'; btn.dataset.uid=grp.uid;

      var title=document.createElement('div'); title.className='rive-group-title'; title.textContent=grp.group+' ('+grp.items.length+')';
      left.appendChild(btn); left.appendChild(title);

      var handle=document.createElement('div'); handle.className='rive-group-handle'; handle.title='拖拽排序此分组'; handle.textContent='⠿'; handle.setAttribute('draggable','true');
      handle.addEventListener('dragstart', onGroupHandleDragStart);
      handle.addEventListener('dragend', onGroupHandleDragEnd);

      gh.appendChild(left); gh.appendChild(handle);
      card.appendChild(gh);

      var items=document.createElement('div'); items.className='rive-items'; if(!open) items.classList.add('hidden');

      for (var ii=0; ii<grp.items.length; ii++){
        var it=grp.items[ii];
        if (filter){
          var ok=(it.key||'').toLowerCase().includes(filter) || (it.value||'').includes(filter);
          if (!ok) continue;
        }
        var row=document.createElement('div'); row.className='rive-row'; row.setAttribute('draggable','true'); row.dataset.uid=it.uid;
        var term=document.createElement('div'); term.className='term'; term.textContent=it.key+' → '+it.value;

        var actions=document.createElement('div'); actions.className='actions';
        var aEdit=document.createElement('a'); aEdit.className='act'; aEdit.href='javascript:void(0);'; aEdit.textContent='编辑'; aEdit.dataset.action='edit';
        var aDel=document.createElement('a'); aDel.className='act'; aDel.href='javascript:void(0);'; aDel.textContent='删除'; aDel.dataset.action='del';

        actions.appendChild(aEdit); actions.appendChild(aDel);
        row.appendChild(term); row.appendChild(actions); items.appendChild(row);

        row.addEventListener('dragstart', onRowDragStart);
        row.addEventListener('dragend', onRowDragEnd);
      }

      card.appendChild(items);
      groupsEl.appendChild(card);
    }
    bindDnDContainer();
  }

  function bindDnDContainer(){
    if (!groupsEl) return;
    groupsEl.onDragOverBound && groupsEl.removeEventListener('dragover', groupsEl.onDragOverBound);
    groupsEl.onDropBound && groupsEl.removeEventListener('drop', groupsEl.onDropBound);

    var onDragOver=function(e){
      e.preventDefault(); if(!DND.mode) return;

      var y = e.clientY + (shadow.host.getBoundingClientRect().top + window.scrollY);
      var rect=groupsEl.getBoundingClientRect();
      var baseY = rect.top + window.scrollY - groupsEl.scrollTop;

      if (DND.mode==='group' && DND.dragGroupEl){
        var groups = Array.prototype.slice.call(groupsEl.querySelectorAll('.rive-group'));
        var insertAt = groups.length;
        for (var i=0;i<groups.length;i++){
          var r=groups[i].getBoundingClientRect(); var mid=r.top + window.scrollY + r.height/2;
          if (y < mid){ insertAt=i; break; }
        }
        var lineTop;
        if (groups.length===0) lineTop=0;
        else if (insertAt===0) lineTop=groups[0].getBoundingClientRect().top + window.scrollY - baseY;
        else if (insertAt>=groups.length) lineTop=groups[groups.length-1].getBoundingClientRect().bottom + window.scrollY - baseY;
        else lineTop=groups[insertAt-1].getBoundingClientRect().bottom + window.scrollY - baseY;
        DND.tgtGroupIdx=insertAt; showIndicator(lineTop);
      }

      if (DND.mode==='item' && DND.dragItemEl){
        var groupEl = DND.dragItemEl.closest('.rive-group'); if(!groupEl) return;
        var rows = Array.prototype.slice.call(groupEl.querySelectorAll('.rive-items > .rive-row')); if (rows.length===0) return;

        var insertAt2 = rows.length;
        for (var j=0;j<rows.length;j++){
          var rr=rows[j].getBoundingClientRect(); var mid2=rr.top + window.scrollY + rr.height/2;
          if (y < mid2){ insertAt2=j; break; }
        }
        var lineTop2;
        if (insertAt2===0) lineTop2=rows[0].getBoundingClientRect().top + window.scrollY - baseY;
        else if (insertAt2>=rows.length) lineTop2=rows[rows.length-1].getBoundingClientRect().bottom + window.scrollY - baseY;
        else lineTop2=rows[insertAt2-1].getBoundingClientRect().bottom + window.scrollY - baseY;
        DND.tgtItemIdx=insertAt2; showIndicator(lineTop2);
      }
    };

    var onDrop=function(e){
      e.preventDefault(); if(!DND.mode) return;

      // 分组拖拽:修正目标索引;更新数据后直接重渲染,避免 DOM 快照错位
      if (DND.mode==='group' && DND.dragGroupEl){
        var groups = Array.prototype.slice.call(groupsEl.querySelectorAll('.rive-group'));
        var src = groups.indexOf(DND.dragGroupEl);
        var dst = DND.tgtGroupIdx != null ? DND.tgtGroupIdx : src;
        if (src < dst && dst < groups.length) dst = dst - 1; // 非末尾插入才补偿
        if (src !== dst && dst >= 0){
          var moved = dict.splice(src,1)[0];
          if (dst > dict.length) dst = dict.length; // 允许 append
          dict.splice(dst,0,moved); saveDict();
          // 关键改动:用 renderGroups() 统一刷新,避免使用旧 rows/refs 造成 off-by-one
          var q1 = shadow.getElementById('rive-search')?.value.trim().toLowerCase() || '';
          renderGroups(q1);
        }
      }

      // 词条拖拽:同理,更新数据后整体重渲染
      if (DND.mode==='item' && DND.dragItemEl){
        var groupEl = DND.dragItemEl.closest('.rive-group'); if(!groupEl) return;
        var gUid = groupEl.dataset.uid;
        var gIdx = dict.findIndex(function(x){ return x.uid===gUid; }); if (gIdx<0) return;

        var rows = Array.prototype.slice.call(groupEl.querySelectorAll('.rive-items > .rive-row'));
        var src2 = rows.indexOf(DND.dragItemEl);
        var dst2 = DND.tgtItemIdx != null ? DND.tgtItemIdx : src2;
        if (src2 < dst2 && dst2 < rows.length) dst2 = dst2 - 1; // 非末尾插入才补偿

        if (src2 !== dst2 && dst2 >= 0){
          var moved2 = dict[gIdx].items.splice(src2,1)[0];
          if (dst2 > dict[gIdx].items.length) dst2 = dict[gIdx].items.length; // 允许 append
          dict[gIdx].items.splice(dst2,0,moved2); saveDict();
          var q2 = shadow.getElementById('rive-search')?.value.trim().toLowerCase() || '';
          renderGroups(q2);
        }
      }

      DND.mode=null; DND.dragGroupEl=null; DND.dragItemEl=null; DND.tgtGroupIdx=null; DND.tgtItemIdx=null;
      hideIndicator();
    };

    groupsEl.onDragOverBound=onDragOver; groupsEl.onDropBound=onDrop;
    groupsEl.addEventListener('dragover', onDragOver);
    groupsEl.addEventListener('drop', onDrop);
  }

  function bindFooter(){
    var add=shadow.getElementById('btn-add');
    var imp=shadow.getElementById('btn-import');
    var exp=shadow.getElementById('btn-export');

    add.addEventListener('click', function(){
      var g=window.prompt('分组名',''); if(!g) return;
      var k=window.prompt('英文术语',''); if(!k) return;
      var v=window.prompt('中文翻译',''); if(v===null||v==='') return;
      var grp=dict.find(function(x){ return x.group===g; });
      if(!grp){ grp={uid:uid(), group:g, items:[]}; dict.push(grp); }
      grp.items.push({uid:uid(), key:k, value:v}); saveDict();
      var q=shadow.getElementById('rive-search').value.trim().toLowerCase(); renderGroups(q);
    });

    exp.addEventListener('click', function(){
      var data=JSON.stringify(dict.map(function(g){
        return { group:g.group, items:g.items.map(function(it){ return {key:it.key, value:it.value}; }) };
      }), null, 2);
      var blob=new Blob([data], {type:'application/json'});
      var url=URL.createObjectURL(blob); var a=document.createElement('a'); a.href=url; a.download='rive_terms.json'; a.click(); URL.revokeObjectURL(url);
    });

    imp.addEventListener('click', function(){
      var input=document.createElement('input'); input.type='file'; input.accept='.json,application/json';
      input.onchange=function(e){
        var file=e.target.files && e.target.files[0]; if(!file) return;
        var reader=new FileReader();
        reader.onload=function(evt){
          try{
            var json=JSON.parse(evt.target.result);
            if(!Array.isArray(json)){ window.alert('JSON 结构应为数组'); return; }
            dict=json.map(function(g){
              return { uid:uid(), group:g.group, items:Array.isArray(g.items)?g.items.map(function(it){ return {uid:uid(), key:it.key, value:it.value}; }):[] };
            });
            saveDict(); var q=shadow.getElementById('rive-search').value.trim().toLowerCase(); renderGroups(q);
          }catch(_){ window.alert('JSON 解析失败'); }
        };
        reader.readAsText(file);
      };
      input.click();
    });
  }

  var lastUrl=location.href;
  function ensurePanel(){ ensureHost(); if(!panelEl && !miniEl) buildPanel(); if(uiState.minimized && panelEl && !miniEl) toMini(); }
  function boot(){ try{ ensurePanel(); }catch(_){ setTimeout(boot,300); } }

  document.addEventListener('keydown', function(e){
    if(e.altKey && e.shiftKey && (e.key==='R' || e.key==='r')){
      try{
        ensureHost(); if(!panelEl) buildPanel();
        if(panelEl) panelEl.classList.remove('hidden');
        if(miniEl){ miniEl.remove(); miniEl=null; }
        uiState.minimized=false; saveUIState();
        setTimeout(function(){ applyEmergencyPanelStyle(panelEl); },50);
      }catch(_){}
    }
  });

  setInterval(function(){ if(lastUrl!==location.href){ lastUrl=location.href; setTimeout(ensurePanel,400); } }, 800);

  var mo=new MutationObserver(function(){ if(!document.getElementById('rive-i18n-host')){ setTimeout(function(){ buildPanel(); },300); } });
  function startObserver(){ if(!document.documentElement){ setTimeout(startObserver,200); return; } mo.observe(document.documentElement,{childList:true,subtree:true}); }

  document.addEventListener('visibilitychange', function(){ if(!document.hidden) setTimeout(ensurePanel,400); });
  setInterval(ensurePanel, 3000);

  if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', function(){ waitForDomReady(boot); }); waitForDomReady(boot); }
  else { waitForDomReady(boot); }
  startObserver();
})();