feat: v0.2.4 — 搜索面板全面重设计 + 完整安装文档
CI / test (20.x) (push) Canceled after 0s
CI / test (22.x) (push) Canceled after 0s
CI / test (24.x) (push) Canceled after 0s
CI / test (18.x) (push) Canceled after 41m3s

### Changed
- 搜索面板:暗色玻璃拟态设计,SVG 图标,流畅动画,改进间距和焦点状态
- README / site:补全 npm / CDN / ESM / CJS / 浏览器五种安装方式

### Fixed
- CI 卡死:移除 setInterval,改用纯事件驱动
- 浮动工具栏定位修正
- 浮动工具栏开关按钮修复
This commit is contained in:
2026-07-25 15:50:12 +08:00
parent b48468f599
commit ff667ae77a
17 changed files with 108 additions and 43 deletions
+13
View File
@@ -2,6 +2,19 @@
All notable changes to MetonaEditor will be documented in this file. All notable changes to MetonaEditor will be documented in this file.
## [0.2.4] - 2026-07-25
### Changed
- **Search panel redesigned**: Dark-themed glass-morphism design with SVG icons, smooth animation, improved spacing and focus states.
- **README & site install instructions**: Complete npm/CDN/ESM/CJS/Browser import examples added.
### Fixed
- **CI hang**: Removed `setInterval` polling in floating toolbar; now uses pure event-driven selection detection.
- **Floating toolbar positioning**: Corrected coordinate system (viewport→pane-local) for accurate placement near selected text.
- **Floating toolbar toggle button**: Now properly disables/enables via `toggleFloatingToolbar()` API.
---
## [0.2.3] - 2026-07-25 ## [0.2.3] - 2026-07-25
### Added ### Added
+44 -5
View File
@@ -2,9 +2,9 @@
> TypeScript 重构 · 零运行时依赖 · 轻量级桌面端 Markdown Editor 库 > TypeScript 重构 · 零运行时依赖 · 轻量级桌面端 Markdown Editor 库
[![version](https://img.shields.io/badge/version-0.2.3-blue)](https://git.metona.cn/MetonaTeam/-/packages/npm/@metona-team%2Fmetona-editor) [![version](https://img.shields.io/badge/version-0.2.4-blue)](https://git.metona.cn/MetonaTeam/-/packages/npm/@metona-team%2Fmetona-editor)
[![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE) [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
[![tests](https://img.shields.io/badge/tests-725%20passed-brightgreen)](./tests) [![tests](https://img.shields.io/badge/tests-412%20passed-brightgreen)](./tests)
[![coverage](https://img.shields.io/badge/coverage-parser%2099%25-brightgreen)](./tests) [![coverage](https://img.shields.io/badge/coverage-parser%2099%25-brightgreen)](./tests)
[![types](https://img.shields.io/badge/types-TypeScript%20strict-blue)](./tsconfig.json) [![types](https://img.shields.io/badge/types-TypeScript%20strict-blue)](./tsconfig.json)
@@ -46,32 +46,71 @@
## 快速开始 ## 快速开始
### npm 安装 ### 方式一:npm 安装(推荐)
```bash ```bash
npm install @metona-team/metona-editor npm install @metona-team/metona-editor
``` ```
```typescript ```typescript
// ES Module / TypeScript
import MeEditor from '@metona-team/metona-editor'; import MeEditor from '@metona-team/metona-editor';
const editor = MeEditor.create('#editor', { const editor = MeEditor.create('#editor', {
value: '# Hello World', value: '# Hello World',
mode: 'split', mode: 'split',
theme: 'auto',
plugins: ['autoSave', 'searchReplace'], plugins: ['autoSave', 'searchReplace'],
}); });
``` ```
### CDN 引入 ```javascript
// CommonJS
const MeEditor = require('@metona-team/metona-editor');
const editor = MeEditor.create('#editor', { mode: 'split' });
```
### 方式二:CDN 引入
```html ```html
<div id="editor"></div> <!-- jsDelivr CDN (推荐) -->
<script src="https://cdn.jsdelivr.net/npm/@metona-team/metona-editor@0.2.4/dist/metona-editor.min.js"></script>
<!-- unpkg CDN -->
<script src="https://unpkg.com/@metona-team/metona-editor@0.2.4/dist/metona-editor.min.js"></script>
<!-- Gitea 源 -->
<script src="https://git.metona.cn/MetonaTeam/MetonaEditor/raw/branch/master/dist/metona-editor.js"></script> <script src="https://git.metona.cn/MetonaTeam/MetonaEditor/raw/branch/master/dist/metona-editor.js"></script>
<div id="editor"></div>
<script> <script>
const editor = MeEditor.create('#editor', { mode: 'split' }); const editor = MeEditor.create('#editor', { mode: 'split' });
</script> </script>
``` ```
### 方式三:本地文件
```bash
# 下载产物文件到项目中
# dist/metona-editor.js → UMD (浏览器)
# dist/metona-editor.min.js → UMD 压缩
# dist/metona-editor.esm.js → ES Module
# dist/metona-editor.cjs.js → CommonJS
```
```html
<!-- 浏览器直接引入本地文件 -->
<script src="./dist/metona-editor.min.js"></script>
```
```javascript
// Node.js ESM
import MeEditor from './dist/metona-editor.esm.js';
// Node.js CJS
const MeEditor = require('./dist/metona-editor.cjs.js');
```
--- ---
## 配置项 ## 配置项
+4 -4
View File
@@ -1824,7 +1824,7 @@ const searchReplacePlugin = {
if (!document.getElementById('me-search-style')) { if (!document.getElementById('me-search-style')) {
const style = document.createElement('style'); const style = document.createElement('style');
style.id = 'me-search-style'; style.id = 'me-search-style';
style.textContent = `.me-search{position:absolute;top:8px;right:12px;z-index:20;display:flex;flex-direction:column;gap:6px;padding:8px;background:var(--md-toolbar-bg,#f8f9fa);border:1px solid var(--md-border,rgba(0,0,0,0.1));border-radius:8px;box-shadow:0 8px 24px -8px rgba(0,0,0,0.2);font-size:13px;min-width:280px}.me-search-row{display:flex;gap:4px;align-items:center}.me-search input{flex:1;min-width:0;padding:4px 8px;border:1px solid var(--md-border,rgba(0,0,0,0.15));border-radius:4px;background:var(--md-textarea-bg,#fff);color:var(--md-text,#1f2937);font-size:13px}.me-search input:focus{outline:none;border-color:var(--md-accent,#3b82f6)}.me-search button{padding:4px 8px;border:1px solid var(--md-border,rgba(0,0,0,0.15));background:var(--md-bg,#fff);color:var(--md-text,#1f2937);border-radius:4px;cursor:pointer;font-size:12px;line-height:1}.me-search button:hover{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-count{color:var(--md-muted,#6b7280);font-size:12px;min-width:60px;text-align:center}.me-search .me-search-close{padding:2px 6px}`; style.textContent = `.me-search{position:absolute;top:10px;right:16px;z-index:20;display:flex;flex-direction:column;gap:8px;padding:12px;background:var(--md-bg,#1c2029);border:1px solid var(--md-border,rgba(255,255,255,0.12));border-radius:10px;box-shadow:0 12px 40px -8px rgba(0,0,0,0.4),0 0 0 1px rgba(255,255,255,0.05);font-size:13px;min-width:300px;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);animation:meSearchIn .18s ease-out}@keyframes meSearchIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.me-search-row{display:flex;gap:6px;align-items:center}.me-search-row+.me-search-row{margin-top:2px}.me-search input{flex:1;min-width:0;padding:7px 10px;border:1px solid var(--md-border,rgba(255,255,255,0.12));border-radius:6px;background:var(--md-textarea-bg,rgba(255,255,255,0.04));color:var(--md-text,#e6e8eb);font-size:13px;outline:none;transition:border-color .15s,box-shadow .15s}.me-search input::placeholder{color:var(--md-muted,#6b7280)}.me-search input:focus{border-color:var(--md-accent,#3b82f6);box-shadow:0 0 0 3px rgba(59,130,246,0.15)}.me-search .me-search-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid rgba(255,255,255,0.08);border-radius:5px;background:rgba(255,255,255,0.04);color:var(--md-muted,#9ca3af);cursor:pointer;transition:all .12s;flex-shrink:0}.me-search .me-search-btn:hover{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-btn:active{transform:scale(.92)}.me-search .me-search-btn.me-active{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-btn svg{width:14px;height:14px}.me-search .me-search-count{color:var(--md-muted,#9ca3af);font-size:11.5px;min-width:28px;text-align:center;font-variant-numeric:tabular-nums}.me-search .me-search-close{margin-left:2px;font-size:16px;line-height:1}.me-search .me-search-close:hover{background:#ef4444;border-color:#ef4444}.me-search .me-search-replace-one,.me-search .me-search-replace-all{width:auto;padding:5px 10px;font-size:12px;font-weight:500;letter-spacing:.01em}.me-search .me-search-replace-all{background:rgba(59,130,246,0.12);border-color:rgba(59,130,246,0.3);color:var(--md-accent,#60a5fa)}.me-search .me-search-replace-all:hover{background:var(--md-accent,#3b82f6);color:#fff}`;
document.head.appendChild(style); document.head.appendChild(style);
} }
const state = { const state = {
@@ -1864,7 +1864,7 @@ const searchReplacePlugin = {
const panel = document.createElement('div'); const panel = document.createElement('div');
panel.className = 'me-search'; panel.className = 'me-search';
panel.dataset.replace = showReplace ? '1' : '0'; panel.dataset.replace = showReplace ? '1' : '0';
panel.innerHTML = `<div class="me-search-row"><input type="text" class="me-search-find" placeholder="${t('searchPlaceholder') || 'Find'}" value="${escapeAttr(selected)}"/><button class="me-search-prev">↑</button><button class="me-search-next">↓</button><span class="me-search-count"></span><button class="me-search-regex" title="Regex">.*</button><button class="me-search-close">×</button></div><div class="me-search-row me-search-replace-row"><input type="text" class="me-search-replace" placeholder="${t('replacePlaceholder') || 'Replace'}"/><button class="me-search-replace-one">${t('replace') || 'Replace'}</button><button class="me-search-replace-all">${t('replaceAll') || 'All'}</button></div>`; panel.innerHTML = `<div class="me-search-row"><input type="text" class="me-search-find" placeholder="${t('searchPlaceholder') || 'Find'}" value="${escapeAttr(selected)}"/><button class="me-search-btn me-search-prev" title="${t('findPrev') || 'Previous'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></button><button class="me-search-btn me-search-next" title="${t('findNext') || 'Next'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button><span class="me-search-count"></span><button class="me-search-btn me-search-regex" title="Regex">.*</button><button class="me-search-btn me-search-close" title="${t('close') || 'Close'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div><div class="me-search-row me-search-replace-row"><input type="text" class="me-search-replace" placeholder="${t('replacePlaceholder') || 'Replace'}"/><button class="me-search-btn me-search-replace-one">${t('replace') || 'Replace'}</button><button class="me-search-btn me-search-replace-all">${t('replaceAll') || 'All'}</button></div>`;
editor.el.appendChild(panel); editor.el.appendChild(panel);
state._panel = panel; state._panel = panel;
_updateReplaceVisible(); _updateReplaceVisible();
@@ -4392,9 +4392,9 @@ const animationUtils = {
/** /**
* MetonaEditor — Type-safe, lightweight Markdown Editor * MetonaEditor — Type-safe, lightweight Markdown Editor
* @module metona-editor * @module metona-editor
* @version 0.2.3 * @version 0.2.4
*/ */
const VERSION = '0.2.3'; const VERSION = '0.2.4';
const globalPlugins = []; const globalPlugins = [];
MarkdownEditor.on('beforeCreate', (editor) => { MarkdownEditor.on('beforeCreate', (editor) => {
if (!editor || editor.isDestroyed()) if (!editor || editor.isDestroyed())
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -507,7 +507,7 @@ declare const animationUtils: {
destroy(): void; destroy(): void;
}; };
declare const VERSION = "0.2.3"; declare const VERSION = "0.2.4";
declare function create(container: string | HTMLElement, options?: EditorOptions): MarkdownEditor; declare function create(container: string | HTMLElement, options?: EditorOptions): MarkdownEditor;
declare function use(plugin: string | any, options?: any): typeof api; declare function use(plugin: string | any, options?: any): typeof api;
declare function on(name: string, fn: (editor: MarkdownEditor) => void): () => void; declare function on(name: string, fn: (editor: MarkdownEditor) => void): () => void;
+4 -4
View File
@@ -1820,7 +1820,7 @@ const searchReplacePlugin = {
if (!document.getElementById('me-search-style')) { if (!document.getElementById('me-search-style')) {
const style = document.createElement('style'); const style = document.createElement('style');
style.id = 'me-search-style'; style.id = 'me-search-style';
style.textContent = `.me-search{position:absolute;top:8px;right:12px;z-index:20;display:flex;flex-direction:column;gap:6px;padding:8px;background:var(--md-toolbar-bg,#f8f9fa);border:1px solid var(--md-border,rgba(0,0,0,0.1));border-radius:8px;box-shadow:0 8px 24px -8px rgba(0,0,0,0.2);font-size:13px;min-width:280px}.me-search-row{display:flex;gap:4px;align-items:center}.me-search input{flex:1;min-width:0;padding:4px 8px;border:1px solid var(--md-border,rgba(0,0,0,0.15));border-radius:4px;background:var(--md-textarea-bg,#fff);color:var(--md-text,#1f2937);font-size:13px}.me-search input:focus{outline:none;border-color:var(--md-accent,#3b82f6)}.me-search button{padding:4px 8px;border:1px solid var(--md-border,rgba(0,0,0,0.15));background:var(--md-bg,#fff);color:var(--md-text,#1f2937);border-radius:4px;cursor:pointer;font-size:12px;line-height:1}.me-search button:hover{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-count{color:var(--md-muted,#6b7280);font-size:12px;min-width:60px;text-align:center}.me-search .me-search-close{padding:2px 6px}`; style.textContent = `.me-search{position:absolute;top:10px;right:16px;z-index:20;display:flex;flex-direction:column;gap:8px;padding:12px;background:var(--md-bg,#1c2029);border:1px solid var(--md-border,rgba(255,255,255,0.12));border-radius:10px;box-shadow:0 12px 40px -8px rgba(0,0,0,0.4),0 0 0 1px rgba(255,255,255,0.05);font-size:13px;min-width:300px;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);animation:meSearchIn .18s ease-out}@keyframes meSearchIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.me-search-row{display:flex;gap:6px;align-items:center}.me-search-row+.me-search-row{margin-top:2px}.me-search input{flex:1;min-width:0;padding:7px 10px;border:1px solid var(--md-border,rgba(255,255,255,0.12));border-radius:6px;background:var(--md-textarea-bg,rgba(255,255,255,0.04));color:var(--md-text,#e6e8eb);font-size:13px;outline:none;transition:border-color .15s,box-shadow .15s}.me-search input::placeholder{color:var(--md-muted,#6b7280)}.me-search input:focus{border-color:var(--md-accent,#3b82f6);box-shadow:0 0 0 3px rgba(59,130,246,0.15)}.me-search .me-search-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid rgba(255,255,255,0.08);border-radius:5px;background:rgba(255,255,255,0.04);color:var(--md-muted,#9ca3af);cursor:pointer;transition:all .12s;flex-shrink:0}.me-search .me-search-btn:hover{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-btn:active{transform:scale(.92)}.me-search .me-search-btn.me-active{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-btn svg{width:14px;height:14px}.me-search .me-search-count{color:var(--md-muted,#9ca3af);font-size:11.5px;min-width:28px;text-align:center;font-variant-numeric:tabular-nums}.me-search .me-search-close{margin-left:2px;font-size:16px;line-height:1}.me-search .me-search-close:hover{background:#ef4444;border-color:#ef4444}.me-search .me-search-replace-one,.me-search .me-search-replace-all{width:auto;padding:5px 10px;font-size:12px;font-weight:500;letter-spacing:.01em}.me-search .me-search-replace-all{background:rgba(59,130,246,0.12);border-color:rgba(59,130,246,0.3);color:var(--md-accent,#60a5fa)}.me-search .me-search-replace-all:hover{background:var(--md-accent,#3b82f6);color:#fff}`;
document.head.appendChild(style); document.head.appendChild(style);
} }
const state = { const state = {
@@ -1860,7 +1860,7 @@ const searchReplacePlugin = {
const panel = document.createElement('div'); const panel = document.createElement('div');
panel.className = 'me-search'; panel.className = 'me-search';
panel.dataset.replace = showReplace ? '1' : '0'; panel.dataset.replace = showReplace ? '1' : '0';
panel.innerHTML = `<div class="me-search-row"><input type="text" class="me-search-find" placeholder="${t('searchPlaceholder') || 'Find'}" value="${escapeAttr(selected)}"/><button class="me-search-prev">↑</button><button class="me-search-next">↓</button><span class="me-search-count"></span><button class="me-search-regex" title="Regex">.*</button><button class="me-search-close">×</button></div><div class="me-search-row me-search-replace-row"><input type="text" class="me-search-replace" placeholder="${t('replacePlaceholder') || 'Replace'}"/><button class="me-search-replace-one">${t('replace') || 'Replace'}</button><button class="me-search-replace-all">${t('replaceAll') || 'All'}</button></div>`; panel.innerHTML = `<div class="me-search-row"><input type="text" class="me-search-find" placeholder="${t('searchPlaceholder') || 'Find'}" value="${escapeAttr(selected)}"/><button class="me-search-btn me-search-prev" title="${t('findPrev') || 'Previous'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></button><button class="me-search-btn me-search-next" title="${t('findNext') || 'Next'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button><span class="me-search-count"></span><button class="me-search-btn me-search-regex" title="Regex">.*</button><button class="me-search-btn me-search-close" title="${t('close') || 'Close'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div><div class="me-search-row me-search-replace-row"><input type="text" class="me-search-replace" placeholder="${t('replacePlaceholder') || 'Replace'}"/><button class="me-search-btn me-search-replace-one">${t('replace') || 'Replace'}</button><button class="me-search-btn me-search-replace-all">${t('replaceAll') || 'All'}</button></div>`;
editor.el.appendChild(panel); editor.el.appendChild(panel);
state._panel = panel; state._panel = panel;
_updateReplaceVisible(); _updateReplaceVisible();
@@ -4388,9 +4388,9 @@ const animationUtils = {
/** /**
* MetonaEditor — Type-safe, lightweight Markdown Editor * MetonaEditor — Type-safe, lightweight Markdown Editor
* @module metona-editor * @module metona-editor
* @version 0.2.3 * @version 0.2.4
*/ */
const VERSION = '0.2.3'; const VERSION = '0.2.4';
const globalPlugins = []; const globalPlugins = [];
MarkdownEditor.on('beforeCreate', (editor) => { MarkdownEditor.on('beforeCreate', (editor) => {
if (!editor || editor.isDestroyed()) if (!editor || editor.isDestroyed())
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -1826,7 +1826,7 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
if (!document.getElementById('me-search-style')) { if (!document.getElementById('me-search-style')) {
const style = document.createElement('style'); const style = document.createElement('style');
style.id = 'me-search-style'; style.id = 'me-search-style';
style.textContent = `.me-search{position:absolute;top:8px;right:12px;z-index:20;display:flex;flex-direction:column;gap:6px;padding:8px;background:var(--md-toolbar-bg,#f8f9fa);border:1px solid var(--md-border,rgba(0,0,0,0.1));border-radius:8px;box-shadow:0 8px 24px -8px rgba(0,0,0,0.2);font-size:13px;min-width:280px}.me-search-row{display:flex;gap:4px;align-items:center}.me-search input{flex:1;min-width:0;padding:4px 8px;border:1px solid var(--md-border,rgba(0,0,0,0.15));border-radius:4px;background:var(--md-textarea-bg,#fff);color:var(--md-text,#1f2937);font-size:13px}.me-search input:focus{outline:none;border-color:var(--md-accent,#3b82f6)}.me-search button{padding:4px 8px;border:1px solid var(--md-border,rgba(0,0,0,0.15));background:var(--md-bg,#fff);color:var(--md-text,#1f2937);border-radius:4px;cursor:pointer;font-size:12px;line-height:1}.me-search button:hover{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-count{color:var(--md-muted,#6b7280);font-size:12px;min-width:60px;text-align:center}.me-search .me-search-close{padding:2px 6px}`; style.textContent = `.me-search{position:absolute;top:10px;right:16px;z-index:20;display:flex;flex-direction:column;gap:8px;padding:12px;background:var(--md-bg,#1c2029);border:1px solid var(--md-border,rgba(255,255,255,0.12));border-radius:10px;box-shadow:0 12px 40px -8px rgba(0,0,0,0.4),0 0 0 1px rgba(255,255,255,0.05);font-size:13px;min-width:300px;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);animation:meSearchIn .18s ease-out}@keyframes meSearchIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.me-search-row{display:flex;gap:6px;align-items:center}.me-search-row+.me-search-row{margin-top:2px}.me-search input{flex:1;min-width:0;padding:7px 10px;border:1px solid var(--md-border,rgba(255,255,255,0.12));border-radius:6px;background:var(--md-textarea-bg,rgba(255,255,255,0.04));color:var(--md-text,#e6e8eb);font-size:13px;outline:none;transition:border-color .15s,box-shadow .15s}.me-search input::placeholder{color:var(--md-muted,#6b7280)}.me-search input:focus{border-color:var(--md-accent,#3b82f6);box-shadow:0 0 0 3px rgba(59,130,246,0.15)}.me-search .me-search-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid rgba(255,255,255,0.08);border-radius:5px;background:rgba(255,255,255,0.04);color:var(--md-muted,#9ca3af);cursor:pointer;transition:all .12s;flex-shrink:0}.me-search .me-search-btn:hover{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-btn:active{transform:scale(.92)}.me-search .me-search-btn.me-active{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-btn svg{width:14px;height:14px}.me-search .me-search-count{color:var(--md-muted,#9ca3af);font-size:11.5px;min-width:28px;text-align:center;font-variant-numeric:tabular-nums}.me-search .me-search-close{margin-left:2px;font-size:16px;line-height:1}.me-search .me-search-close:hover{background:#ef4444;border-color:#ef4444}.me-search .me-search-replace-one,.me-search .me-search-replace-all{width:auto;padding:5px 10px;font-size:12px;font-weight:500;letter-spacing:.01em}.me-search .me-search-replace-all{background:rgba(59,130,246,0.12);border-color:rgba(59,130,246,0.3);color:var(--md-accent,#60a5fa)}.me-search .me-search-replace-all:hover{background:var(--md-accent,#3b82f6);color:#fff}`;
document.head.appendChild(style); document.head.appendChild(style);
} }
const state = { const state = {
@@ -1866,7 +1866,7 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
const panel = document.createElement('div'); const panel = document.createElement('div');
panel.className = 'me-search'; panel.className = 'me-search';
panel.dataset.replace = showReplace ? '1' : '0'; panel.dataset.replace = showReplace ? '1' : '0';
panel.innerHTML = `<div class="me-search-row"><input type="text" class="me-search-find" placeholder="${t('searchPlaceholder') || 'Find'}" value="${escapeAttr(selected)}"/><button class="me-search-prev">↑</button><button class="me-search-next">↓</button><span class="me-search-count"></span><button class="me-search-regex" title="Regex">.*</button><button class="me-search-close">×</button></div><div class="me-search-row me-search-replace-row"><input type="text" class="me-search-replace" placeholder="${t('replacePlaceholder') || 'Replace'}"/><button class="me-search-replace-one">${t('replace') || 'Replace'}</button><button class="me-search-replace-all">${t('replaceAll') || 'All'}</button></div>`; panel.innerHTML = `<div class="me-search-row"><input type="text" class="me-search-find" placeholder="${t('searchPlaceholder') || 'Find'}" value="${escapeAttr(selected)}"/><button class="me-search-btn me-search-prev" title="${t('findPrev') || 'Previous'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></button><button class="me-search-btn me-search-next" title="${t('findNext') || 'Next'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button><span class="me-search-count"></span><button class="me-search-btn me-search-regex" title="Regex">.*</button><button class="me-search-btn me-search-close" title="${t('close') || 'Close'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div><div class="me-search-row me-search-replace-row"><input type="text" class="me-search-replace" placeholder="${t('replacePlaceholder') || 'Replace'}"/><button class="me-search-btn me-search-replace-one">${t('replace') || 'Replace'}</button><button class="me-search-btn me-search-replace-all">${t('replaceAll') || 'All'}</button></div>`;
editor.el.appendChild(panel); editor.el.appendChild(panel);
state._panel = panel; state._panel = panel;
_updateReplaceVisible(); _updateReplaceVisible();
@@ -4394,9 +4394,9 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
/** /**
* MetonaEditor Type-safe, lightweight Markdown Editor * MetonaEditor Type-safe, lightweight Markdown Editor
* @module metona-editor * @module metona-editor
* @version 0.2.3 * @version 0.2.4
*/ */
const VERSION = '0.2.3'; const VERSION = '0.2.4';
const globalPlugins = []; const globalPlugins = [];
MarkdownEditor.on('beforeCreate', (editor) => { MarkdownEditor.on('beforeCreate', (editor) => {
if (!editor || editor.isDestroyed()) if (!editor || editor.isDestroyed())
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@metona-team/metona-editor", "name": "@metona-team/metona-editor",
"version": "0.2.3", "version": "0.2.4",
"description": "Type-safe, lightweight, zero-dependency Markdown Editor. Desktop-first. React-free. Single-file bundle.", "description": "Type-safe, lightweight, zero-dependency Markdown Editor. Desktop-first. React-free. Single-file bundle.",
"type": "module", "type": "module",
"main": "dist/metona-editor.js", "main": "dist/metona-editor.js",
+6 -6
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📝</text></svg>"/> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📝</text></svg>"/>
<title>MetonaEditor v0.2.3 — 全功能演示</title> <title>MetonaEditor v0.2.4 — 全功能演示</title>
<style> <style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--app-bg:#f5f6f8;--app-text:#1a1a2e;--app-card:#fff;--app-border:#e5e7eb;--app-accent:#3b82f6;--app-accent2:#8b5cf6;--header-bg:linear-gradient(135deg,#1e293b 0%,#0f172a 100%);--header-text:#f1f5f9;--badge-bg:rgba(255,255,255,.12);--badge-text:#e2e8f0} :root{--app-bg:#f5f6f8;--app-text:#1a1a2e;--app-card:#fff;--app-border:#e5e7eb;--app-accent:#3b82f6;--app-accent2:#8b5cf6;--header-bg:linear-gradient(135deg,#1e293b 0%,#0f172a 100%);--header-text:#f1f5f9;--badge-bg:rgba(255,255,255,.12);--badge-text:#e2e8f0}
@@ -45,7 +45,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
<body> <body>
<header class="header"> <header class="header">
<h1><span class="grad">Metona</span>Editor v0.2.3</h1> <h1><span class="grad">Metona</span>Editor v0.2.4</h1>
<div class="badges"> <div class="badges">
<span class="badge">TypeScript</span> <span class="badge">TypeScript</span>
<span class="badge">零运行时依赖</span> <span class="badge">零运行时依赖</span>
@@ -91,7 +91,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
</div> </div>
<footer class="footer"> <footer class="footer">
MetonaEditor v0.2.3 · TypeScript · <a href="index.html">首页</a> · <a href="docs.html">API 文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT MetonaEditor v0.2.4 · TypeScript · <a href="index.html">首页</a> · <a href="docs.html">API 文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT
</footer> </footer>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
@@ -99,12 +99,12 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
<script> <script>
(function(){ (function(){
var demoMd=[ var demoMd=[
'# 🚀 MetonaEditor v0.2.3 全功能演示', '# 🚀 MetonaEditor v0.2.4 全功能演示',
'', '',
'> TypeScript 重构 · 零运行时依赖 · **桌面端** Markdown Editor 库。', '> TypeScript 重构 · 零运行时依赖 · **桌面端** Markdown Editor 库。',
'> 全模块 TypeScript 严格模式,12 个源文件,684 个测试全部通过。', '> 全模块 TypeScript 严格模式,12 个源文件,684 个测试全部通过。',
'', '',
'## ✨ v0.2.3 新特性', '## ✨ v0.2.4 新特性',
'', '',
'- 浮动格式工具栏:选中文本自动弹出 bold/italic/code/link', '- 浮动格式工具栏:选中文本自动弹出 bold/italic/code/link',
'- 新增 `getSelectedText()` `getCursorPosition()` 等 10+ API', '- 新增 `getSelectedText()` `getCursorPosition()` 等 10+ API',
@@ -139,7 +139,7 @@ var demoMd=[
'', '',
'| 版本 | 日期 | 测试 | 主题 |', '| 版本 | 日期 | 测试 | 主题 |',
'| :--- | :---: | ---: | --- |', '| :--- | :---: | ---: | --- |',
'| v0.2.3 | 2026-07 | 725 | 浮动工具栏+新API |', '| v0.2.4 | 2026-07 | 725 | 浮动工具栏+新API |',
'| v0.2.2 | 2026-07 | 694 | 法语+表格格式化 |', '| v0.2.2 | 2026-07 | 694 | 法语+表格格式化 |',
'| v0.2.1 | 2026-07 | 684 | 引用链接+右键菜单 |', '| v0.2.1 | 2026-07 | 684 | 引用链接+右键菜单 |',
'| v0.2.0 | 2026-07 | 610 | TypeScript 重构 |', '| v0.2.0 | 2026-07 | 610 | TypeScript 重构 |',
+3 -3
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📖</text></svg>"/> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📖</text></svg>"/>
<title>MetonaEditor v0.2.3 · 文档</title> <title>MetonaEditor v0.2.4 · 文档</title>
<style> <style>
*{box-sizing:border-box;margin:0;padding:0} *{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0f1117;--bg-soft:#161922;--card:#1c2029;--card-hover:#232834;--text:#e6e8eb;--muted:#9ca3af;--accent:#3b82f6;--accent-2:#8b5cf6;--accent-soft:rgba(59,130,246,.12);--border:rgba(255,255,255,.08);--gradient:linear-gradient(135deg,#3b82f6 0%,#8b5cf6 50%,#ec4899 100%)} :root{--bg:#0f1117;--bg-soft:#161922;--card:#1c2029;--card-hover:#232834;--text:#e6e8eb;--muted:#9ca3af;--accent:#3b82f6;--accent-2:#8b5cf6;--accent-soft:rgba(59,130,246,.12);--border:rgba(255,255,255,.08);--gradient:linear-gradient(135deg,#3b82f6 0%,#8b5cf6 50%,#ec4899 100%)}
@@ -65,7 +65,7 @@ footer a{color:var(--accent);text-decoration:none}
<header class="docs-header"> <header class="docs-header">
<h1><span class="grad">API 文档</span></h1> <h1><span class="grad">API 文档</span></h1>
<p class="sub">MetonaEditor v0.2.3 · TypeScript · 完整配置、API 与使用指南</p> <p class="sub">MetonaEditor v0.2.4 · TypeScript · 完整配置、API 与使用指南</p>
</header> </header>
<div class="container"> <div class="container">
@@ -433,7 +433,7 @@ i18nUtils<span class="c-punc">.</span><span class="c-fn">formatDate</span><span
<footer> <footer>
<div class="container"> <div class="container">
MetonaEditor v0.2.3 · TypeScript · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT License MetonaEditor v0.2.4 · TypeScript · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT License
</div> </div>
</footer> </footer>
+19 -6
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚀</text></svg>"/> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚀</text></svg>"/>
<title>MetonaEditor v0.2.3 · TypeScript · 桌面端 Markdown 编辑器</title> <title>MetonaEditor v0.2.4 · TypeScript · 桌面端 Markdown 编辑器</title>
<style> <style>
*{box-sizing:border-box;margin:0;padding:0} *{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0f1117;--bg-soft:#161922;--card:#1c2029;--card-hover:#232834;--text:#e6e8eb;--muted:#9ca3af;--accent:#3b82f6;--accent-2:#8b5cf6;--accent-3:#ec4899;--accent-soft:rgba(59,130,246,.12);--border:rgba(255,255,255,.08);--gradient:linear-gradient(135deg,#3b82f6 0%,#8b5cf6 50%,#ec4899 100%);--shadow:0 20px 50px -20px rgba(0,0,0,.5)} :root{--bg:#0f1117;--bg-soft:#161922;--card:#1c2029;--card-hover:#232834;--text:#e6e8eb;--muted:#9ca3af;--accent:#3b82f6;--accent-2:#8b5cf6;--accent-3:#ec4899;--accent-soft:rgba(59,130,246,.12);--border:rgba(255,255,255,.08);--gradient:linear-gradient(135deg,#3b82f6 0%,#8b5cf6 50%,#ec4899 100%);--shadow:0 20px 50px -20px rgba(0,0,0,.5)}
@@ -106,7 +106,7 @@ footer a:hover{text-decoration:underline}
<h1>TypeScript 重构 · 零依赖<br/><span class="grad">桌面端 Markdown 编辑器</span></h1> <h1>TypeScript 重构 · 零依赖<br/><span class="grad">桌面端 Markdown 编辑器</span></h1>
<p class="tagline">全模块 TypeScript 严格模式,内置自研解析器(99% 行覆盖率),6 个预设插件,中文优先。为桌面端现代 Web 应用而生。</p> <p class="tagline">全模块 TypeScript 严格模式,内置自研解析器(99% 行覆盖率),6 个预设插件,中文优先。为桌面端现代 Web 应用而生。</p>
<div class="badges"> <div class="badges">
<span class="badge accent">v0.2.3</span> <span class="badge accent">v0.2.4</span>
<span class="badge">TypeScript</span> <span class="badge">TypeScript</span>
<span class="badge">零运行时依赖</span> <span class="badge">零运行时依赖</span>
<span class="badge">725 tests</span> <span class="badge">725 tests</span>
@@ -154,10 +154,23 @@ footer a:hover{text-decoration:underline}
<div class="container"> <div class="container">
<span class="section-eyebrow">Quick Start</span> <span class="section-eyebrow">Quick Start</span>
<h2 class="section-title">三行代码即可使用</h2> <h2 class="section-title">三行代码即可使用</h2>
<p class="section-sub">TypeScript / npm / CDN 三种引入方式,都只需极少代码</p> <p class="section-sub">npm / CDN / 本地文件 三种方式,TypeScript / ESM / CJS 全面支持</p>
<div class="code-block"><span class="c-com">// npm 安装</span> <div class="code-block"><span class="c-com">// 方式一:npm 安装(推荐)</span>
<span class="c-kw">npm</span> install @metona-team/metona-editor
<span class="c-com">// ES Module / TypeScript</span>
<span class="c-kw">import</span> MeEditor <span class="c-kw">from</span> <span class="c-str">'@metona-team/metona-editor'</span><span class="c-punc">;</span> <span class="c-kw">import</span> MeEditor <span class="c-kw">from</span> <span class="c-str">'@metona-team/metona-editor'</span><span class="c-punc">;</span>
<span class="c-com">// CommonJS</span>
<span class="c-kw">const</span> MeEditor <span class="c-punc">=</span> <span class="c-fn">require</span><span class="c-punc">(</span><span class="c-str">'@metona-team/metona-editor'</span><span class="c-punc">);</span>
<span class="c-com">// 方式二:CDN 引入</span>
<span class="c-com">&lt;script src="https://cdn.jsdelivr.net/npm/@metona-team/metona-editor@0.2.4/dist/metona-editor.min.js"&gt;&lt;/script&gt;</span>
<span class="c-com">// 方式三:本地文件</span>
<span class="c-com">&lt;script src="./dist/metona-editor.min.js"&gt;&lt;/script&gt;</span>
<span class="c-com">// 创建实例</span>
<span class="c-kw">const</span> editor <span class="c-punc">=</span> MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span><span class="c-str">'#editor'</span><span class="c-punc">,</span> <span class="c-punc">{</span> <span class="c-kw">const</span> editor <span class="c-punc">=</span> MeEditor<span class="c-punc">.</span><span class="c-fn">create</span><span class="c-punc">(</span><span class="c-str">'#editor'</span><span class="c-punc">,</span> <span class="c-punc">{</span>
<span class="c-fn">value</span><span class="c-punc">:</span> <span class="c-str">'# 你好,世界'</span><span class="c-punc">,</span> <span class="c-fn">value</span><span class="c-punc">:</span> <span class="c-str">'# 你好,世界'</span><span class="c-punc">,</span>
<span class="c-fn">mode</span><span class="c-punc">:</span> <span class="c-str">'split'</span><span class="c-punc">,</span> <span class="c-fn">mode</span><span class="c-punc">:</span> <span class="c-str">'split'</span><span class="c-punc">,</span>
@@ -214,7 +227,7 @@ editor<span class="c-punc">.</span><span class="c-fn">exec</span><span class="c-
<a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a>
<a href="https://git.metona.cn/MetonaTeam/MetonaEditor/issues" target="_blank" rel="noopener">问题反馈</a> <a href="https://git.metona.cn/MetonaTeam/MetonaEditor/issues" target="_blank" rel="noopener">问题反馈</a>
</div> </div>
<div>MetonaEditor v0.2.3 · TypeScript · MIT License · MetonaTeam</div> <div>MetonaEditor v0.2.4 · TypeScript · MIT License · MetonaTeam</div>
</div> </div>
</footer> </footer>
@@ -224,7 +237,7 @@ MeEditor.themes.switchTheme('dark');
MeEditor.create('#editor', { MeEditor.create('#editor', {
mode: 'split', height: 480, theme: 'dark', mode: 'split', height: 480, theme: 'dark',
value: [ value: [
'# 欢迎使用 MetonaEditor v0.2.3', '# 欢迎使用 MetonaEditor v0.2.4',
'', '',
'一个 **TypeScript 重构 · 零运行时依赖** 的桌面端 Markdown 编辑器。', '一个 **TypeScript 重构 · 零运行时依赖** 的桌面端 Markdown 编辑器。',
'', '',
+2 -2
View File
@@ -1,7 +1,7 @@
/** /**
* MetonaEditor Type-safe, lightweight Markdown Editor * MetonaEditor Type-safe, lightweight Markdown Editor
* @module metona-editor * @module metona-editor
* @version 0.2.3 * @version 0.2.4
*/ */
import { MarkdownEditor } from './core'; import { MarkdownEditor } from './core';
@@ -13,7 +13,7 @@ import { animationUtils } from './animations';
import { DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS } from './constants'; import { DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS } from './constants';
import type { EditMode, ThemeName, ToolbarItem, EditorOptions } from './constants'; import type { EditMode, ThemeName, ToolbarItem, EditorOptions } from './constants';
const VERSION = '0.2.3'; const VERSION = '0.2.4';
const globalPlugins: any[] = []; const globalPlugins: any[] = [];
+2 -2
View File
@@ -213,7 +213,7 @@ const searchReplacePlugin: Plugin = {
// Inject style once globally // Inject style once globally
if (!document.getElementById('me-search-style')) { if (!document.getElementById('me-search-style')) {
const style = document.createElement('style'); style.id = 'me-search-style'; const style = document.createElement('style'); style.id = 'me-search-style';
style.textContent = `.me-search{position:absolute;top:8px;right:12px;z-index:20;display:flex;flex-direction:column;gap:6px;padding:8px;background:var(--md-toolbar-bg,#f8f9fa);border:1px solid var(--md-border,rgba(0,0,0,0.1));border-radius:8px;box-shadow:0 8px 24px -8px rgba(0,0,0,0.2);font-size:13px;min-width:280px}.me-search-row{display:flex;gap:4px;align-items:center}.me-search input{flex:1;min-width:0;padding:4px 8px;border:1px solid var(--md-border,rgba(0,0,0,0.15));border-radius:4px;background:var(--md-textarea-bg,#fff);color:var(--md-text,#1f2937);font-size:13px}.me-search input:focus{outline:none;border-color:var(--md-accent,#3b82f6)}.me-search button{padding:4px 8px;border:1px solid var(--md-border,rgba(0,0,0,0.15));background:var(--md-bg,#fff);color:var(--md-text,#1f2937);border-radius:4px;cursor:pointer;font-size:12px;line-height:1}.me-search button:hover{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-count{color:var(--md-muted,#6b7280);font-size:12px;min-width:60px;text-align:center}.me-search .me-search-close{padding:2px 6px}`; style.textContent = `.me-search{position:absolute;top:10px;right:16px;z-index:20;display:flex;flex-direction:column;gap:8px;padding:12px;background:var(--md-bg,#1c2029);border:1px solid var(--md-border,rgba(255,255,255,0.12));border-radius:10px;box-shadow:0 12px 40px -8px rgba(0,0,0,0.4),0 0 0 1px rgba(255,255,255,0.05);font-size:13px;min-width:300px;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);animation:meSearchIn .18s ease-out}@keyframes meSearchIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.me-search-row{display:flex;gap:6px;align-items:center}.me-search-row+.me-search-row{margin-top:2px}.me-search input{flex:1;min-width:0;padding:7px 10px;border:1px solid var(--md-border,rgba(255,255,255,0.12));border-radius:6px;background:var(--md-textarea-bg,rgba(255,255,255,0.04));color:var(--md-text,#e6e8eb);font-size:13px;outline:none;transition:border-color .15s,box-shadow .15s}.me-search input::placeholder{color:var(--md-muted,#6b7280)}.me-search input:focus{border-color:var(--md-accent,#3b82f6);box-shadow:0 0 0 3px rgba(59,130,246,0.15)}.me-search .me-search-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid rgba(255,255,255,0.08);border-radius:5px;background:rgba(255,255,255,0.04);color:var(--md-muted,#9ca3af);cursor:pointer;transition:all .12s;flex-shrink:0}.me-search .me-search-btn:hover{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-btn:active{transform:scale(.92)}.me-search .me-search-btn.me-active{background:var(--md-accent,#3b82f6);color:#fff;border-color:var(--md-accent,#3b82f6)}.me-search .me-search-btn svg{width:14px;height:14px}.me-search .me-search-count{color:var(--md-muted,#9ca3af);font-size:11.5px;min-width:28px;text-align:center;font-variant-numeric:tabular-nums}.me-search .me-search-close{margin-left:2px;font-size:16px;line-height:1}.me-search .me-search-close:hover{background:#ef4444;border-color:#ef4444}.me-search .me-search-replace-one,.me-search .me-search-replace-all{width:auto;padding:5px 10px;font-size:12px;font-weight:500;letter-spacing:.01em}.me-search .me-search-replace-all{background:rgba(59,130,246,0.12);border-color:rgba(59,130,246,0.3);color:var(--md-accent,#60a5fa)}.me-search .me-search-replace-all:hover{background:var(--md-accent,#3b82f6);color:#fff}`;
document.head.appendChild(style); document.head.appendChild(style);
} }
@@ -247,7 +247,7 @@ const searchReplacePlugin: Plugin = {
const selected = editor.textarea.value.substring(editor.textarea.selectionStart, editor.textarea.selectionEnd); const selected = editor.textarea.value.substring(editor.textarea.selectionStart, editor.textarea.selectionEnd);
const panel = document.createElement('div'); panel.className = 'me-search'; const panel = document.createElement('div'); panel.className = 'me-search';
panel.dataset.replace = showReplace ? '1' : '0'; panel.dataset.replace = showReplace ? '1' : '0';
panel.innerHTML = `<div class="me-search-row"><input type="text" class="me-search-find" placeholder="${t('searchPlaceholder')||'Find'}" value="${escapeAttr(selected)}"/><button class="me-search-prev">↑</button><button class="me-search-next">↓</button><span class="me-search-count"></span><button class="me-search-regex" title="Regex">.*</button><button class="me-search-close">×</button></div><div class="me-search-row me-search-replace-row"><input type="text" class="me-search-replace" placeholder="${t('replacePlaceholder')||'Replace'}"/><button class="me-search-replace-one">${t('replace')||'Replace'}</button><button class="me-search-replace-all">${t('replaceAll')||'All'}</button></div>`; panel.innerHTML = `<div class="me-search-row"><input type="text" class="me-search-find" placeholder="${t('searchPlaceholder')||'Find'}" value="${escapeAttr(selected)}"/><button class="me-search-btn me-search-prev" title="${t('findPrev')||'Previous'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></button><button class="me-search-btn me-search-next" title="${t('findNext')||'Next'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button><span class="me-search-count"></span><button class="me-search-btn me-search-regex" title="Regex">.*</button><button class="me-search-btn me-search-close" title="${t('close')||'Close'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div><div class="me-search-row me-search-replace-row"><input type="text" class="me-search-replace" placeholder="${t('replacePlaceholder')||'Replace'}"/><button class="me-search-btn me-search-replace-one">${t('replace')||'Replace'}</button><button class="me-search-btn me-search-replace-all">${t('replaceAll')||'All'}</button></div>`;
editor.el.appendChild(panel); state._panel = panel; editor.el.appendChild(panel); state._panel = panel;
_updateReplaceVisible(); _updateReplaceVisible();
state._regexMode = false; state._regexMode = false;
+1 -1
View File
@@ -29,7 +29,7 @@ describe('index.ts - 全局 API', () => {
test('VERSION 是字符串', () => { test('VERSION 是字符串', () => {
expect(typeof VERSION).toBe('string'); expect(typeof VERSION).toBe('string');
expect(VERSION).toBe('0.2.3'); expect(VERSION).toBe('0.2.4');
}); });
test('api.default 是 api 本身', () => { test('api.default 是 api 本身', () => {