Files
MetonaEditor/src/styles.js
T
thzxx 5d9b934a2d fix(core): fix gutter scroll desync with textarea content
- Match gutter font-size to textarea (13.5px) so line heights align
- Always sync gutter scrollTop regardless of mode
- Sync gutter scroll position after undo/redo history apply
2026-07-24 11:20:47 +08:00

585 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* MetonaEditor Styles - 编辑器样式
* @module styles
* @version 0.1.8
* @description 编辑器 UI 样式注入与管理
*/
import { THEMES } from './constants.js';
let styleElement = null;
/**
* 生成 CSS 样式
*/
const generateCSS = () => {
return `
/* ============ CSS 变量默认值(被 themes.js 动态覆盖) ============ */
:root {
--md-bg: #ffffff;
--md-text: #1f2937;
--md-border: rgba(0, 0, 0, 0.08);
--md-shadow: 0 10px 36px -10px rgba(0,0,0,0.18), 0 4px 14px -4px rgba(0,0,0,0.08);
--md-hover-shadow: 0 14px 48px -10px rgba(0,0,0,0.22), 0 6px 18px -4px rgba(0,0,0,0.10);
--md-toolbar-bg: rgba(248, 249, 250, 0.92);
--md-textarea-bg: #ffffff;
--md-preview-bg: #ffffff;
--md-code-bg: rgba(243, 244, 246, 1);
--md-code-text: #1f2937;
--md-accent: #3b82f6;
--md-muted: #6b7280;
--md-radius: 10px;
--md-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", sans-serif;
--md-mono: "SF Mono", "Cascadia Code", "Consolas", "Liberation Mono", "Courier New", monospace;
}
/* ============ 容器 ============ */
.me-wrapper {
display: flex;
flex-direction: column;
box-sizing: border-box;
border: 1px solid var(--md-border);
border-radius: var(--md-radius);
overflow: hidden;
background: var(--md-bg);
color: var(--md-text);
box-shadow: var(--md-shadow);
font-family: var(--md-font);
font-size: 14px;
line-height: 1.6;
position: relative;
transition: box-shadow 0.25s ease, border-color 0.25s ease;
width: 100%;
}
.me-wrapper:hover { box-shadow: var(--md-hover-shadow); }
.me-wrapper.me-disabled { opacity: 0.6; pointer-events: none; }
.me-wrapper * { box-sizing: border-box; }
.me-wrapper.me-fullscreen {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
width: 100vw; height: 100vh;
z-index: 9999;
border-radius: 0;
border: 0;
}
/* ============ 工具栏 ============ */
.me-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 2px;
padding: 6px 8px;
background: var(--md-toolbar-bg);
border-bottom: 1px solid var(--md-border);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
min-height: 40px;
}
.me-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px; height: 30px;
padding: 0;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--md-text);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, transform 0.1s ease;
flex-shrink: 0;
}
.me-btn:hover { background: var(--md-code-bg); color: var(--md-accent); }
.me-btn:active { transform: scale(0.92); }
.me-btn.me-active { background: var(--md-accent); color: #fff; }
.me-btn:disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }
.me-btn:focus-visible { outline: 2px solid var(--md-accent); outline-offset: 1px; }
.me-btn svg { width: 17px; height: 17px; display: block; }
.me-btn span { font-size: 12px; font-weight: 500; }
.me-toolbar-sep {
display: inline-block;
width: 1px; height: 20px;
background: var(--md-border);
margin: 0 4px;
flex-shrink: 0;
}
.me-toolbar-group {
display: inline-flex;
gap: 2px;
margin-left: auto;
padding-left: 6px;
border-left: 1px solid var(--md-border);
}
.me-toolbar-group .me-btn { width: auto; padding: 0 8px; }
.me-toolbar-group .me-btn span { font-size: 12px; }
/* ============ 主体布局 ============ */
.me-body {
display: flex;
flex: 1;
min-height: 0;
position: relative;
}
.me-editor-pane, .me-preview-pane {
flex: 1 1 50%;
min-width: 0;
overflow: hidden;
position: relative;
}
/* ============ 编辑区内层(v0.1.7 ============ */
.me-editor-inner { display: flex; height: 100%; overflow: hidden; position: relative; }
.me-gutter {
flex: 0 0 auto;
min-width: 36px;
padding: 16px 8px 16px 6px;
overflow: hidden;
background: var(--md-code-bg);
border-right: 1px solid var(--md-border);
color: var(--md-muted);
font-family: var(--md-mono);
font-size: 13.5px;
line-height: 1.7;
text-align: right;
user-select: none;
white-space: pre;
}
.me-gutter-line { padding-right: 4px; }
.me-gutter-active {
color: var(--md-accent);
font-weight: 600;
background: rgba(59, 130, 246, 0.08);
border-radius: 3px;
}
.me-divider {
flex: 0 0 1px;
background: var(--md-border);
cursor: col-resize;
position: relative;
}
.me-divider::after {
content: '';
position: absolute;
top: 0; bottom: 0; left: -3px; right: -3px;
}
.me-divider:hover { background: var(--md-accent); }
/* 编辑区 */
.me-textarea {
width: 100%;
height: 100%;
padding: 16px 18px;
border: 0;
outline: 0;
resize: none;
background: var(--md-textarea-bg);
color: var(--md-text);
font-family: var(--md-mono);
font-size: 13.5px;
line-height: 1.7;
tab-size: 2;
-moz-tab-size: 2;
display: block;
white-space: pre-wrap;
word-wrap: break-word;
}
.me-textarea::placeholder { color: var(--md-muted); opacity: 0.7; }
.me-textarea:focus { outline: 0; }
/* 预览区 */
.me-preview-pane { overflow: auto; background: var(--md-preview-bg); }
.me-preview {
padding: 18px 22px;
max-width: 100%;
word-wrap: break-word;
overflow-wrap: break-word;
}
/* 模式切换 */
.me-body.me-mode-edit .me-preview-pane,
.me-body.me-mode-edit .me-divider { display: none; }
.me-body.me-mode-edit .me-editor-pane { flex: 1 1 100%; }
.me-body.me-mode-preview .me-editor-pane,
.me-body.me-mode-preview .me-divider { display: none; }
.me-body.me-mode-preview .me-preview-pane { flex: 1 1 100%; }
/* ============ 预览区排版 ============ */
.me-preview > :first-child { margin-top: 0; }
.me-preview > :last-child { margin-bottom: 0; }
.me-preview h1, .me-preview h2, .me-preview h3,
.me-preview h4, .me-preview h5, .me-preview h6 {
margin: 1.4em 0 0.6em;
font-weight: 650;
line-height: 1.3;
}
.me-preview h1 { font-size: 1.9em; padding-bottom: 0.3em; border-bottom: 1px solid var(--md-border); }
.me-preview h2 { font-size: 1.55em; padding-bottom: 0.3em; border-bottom: 1px solid var(--md-border); }
.me-preview h3 { font-size: 1.3em; }
.me-preview h4 { font-size: 1.12em; }
.me-preview h5 { font-size: 1em; }
.me-preview h6 { font-size: 0.9em; color: var(--md-muted); }
.me-preview p { margin: 0.7em 0; }
.me-preview a { color: var(--md-accent); text-decoration: none; }
.me-preview a:hover { text-decoration: underline; }
.me-preview strong { font-weight: 650; }
.me-preview em { font-style: italic; }
.me-preview del { text-decoration: line-through; opacity: 0.75; }
.me-preview ul, .me-preview ol { margin: 0.6em 0; padding-left: 1.6em; }
.me-preview li { margin: 0.25em 0; }
.me-preview li.me-task-item { list-style: none; margin-left: -1.4em; }
.me-preview li.me-task-item input { margin-right: 0.5em; vertical-align: middle; }
.me-preview blockquote {
margin: 0.8em 0;
padding: 0.4em 1em;
border-left: 3px solid var(--md-accent);
background: var(--md-code-bg);
border-radius: 0 6px 6px 0;
color: var(--md-text);
}
.me-preview blockquote > :first-child { margin-top: 0; }
.me-preview blockquote > :last-child { margin-bottom: 0; }
.me-preview hr {
border: 0;
height: 1px;
background: var(--md-border);
margin: 1.6em 0;
}
.me-preview code {
font-family: var(--md-mono);
font-size: 0.88em;
padding: 0.15em 0.4em;
background: var(--md-code-bg);
color: var(--md-code-text);
border-radius: 4px;
}
.me-preview pre {
margin: 0.9em 0;
padding: 14px 16px;
background: var(--md-code-bg);
border-radius: 8px;
overflow-x: auto;
border: 1px solid var(--md-border);
}
.me-preview pre code {
padding: 0;
background: transparent;
color: var(--md-code-text);
font-size: 0.9em;
line-height: 1.6;
border-radius: 0;
}
.me-preview img {
max-width: 100%;
height: auto;
border-radius: 6px;
vertical-align: middle;
}
.me-table-wrap { overflow-x: auto; margin: 0.9em 0; }
.me-preview table {
border-collapse: collapse;
width: 100%;
font-size: 0.93em;
display: block;
}
.me-preview th, .me-preview td {
border: 1px solid var(--md-border);
padding: 7px 12px;
text-align: left;
}
.me-preview th {
background: var(--md-code-bg);
font-weight: 600;
}
.me-preview tr:nth-child(even) td { background: var(--md-code-bg); }
.me-preview tr:nth-child(even) td { background-color: color-mix(in srgb, var(--md-code-bg) 40%, transparent); }
/* ============ 状态栏 ============ */
.me-statusbar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 14px;
padding: 4px 12px;
border-top: 1px solid var(--md-border);
background: var(--md-toolbar-bg);
color: var(--md-muted);
font-size: 12px;
min-height: 26px;
}
.me-statusbar:empty { display: none; }
/* ============ 响应式 ============ */
@media (max-width: 768px) {
.me-body.me-mode-split {
flex-direction: column;
}
.me-body.me-mode-split .me-divider {
flex: 0 0 1px;
width: 100%;
height: 1px;
cursor: row-resize;
}
.me-body.me-mode-split .me-divider::after {
top: -3px; bottom: -3px; left: 0; right: 0;
}
.me-body.me-mode-split .me-editor-pane,
.me-body.me-mode-split .me-preview-pane {
flex: 1 1 50%;
width: 100%;
}
.me-toolbar { padding: 4px; }
.me-btn { width: 28px; height: 28px; }
.me-preview { padding: 14px; }
.me-textarea { padding: 12px; font-size: 13px; }
}
/* ============ 无障碍 ============ */
.me-wrapper:focus-within {
border-color: var(--md-accent);
}
.me-textarea:focus-visible { outline: 0; }
/* ============ 动画 ============ */
@media (prefers-reduced-motion: reduce) {
.me-wrapper, .me-btn, .me-textarea { transition: none !important; }
}
/* ============ 全屏模式优化 ============ */
.me-wrapper.me-fullscreen .me-preview,
.me-wrapper.me-fullscreen .me-textarea { font-size: 15px; }
/* ============ 只读模式 ============ */
.me-wrapper.me-readonly .me-textarea {
background: var(--md-code-bg);
cursor: default;
opacity: 0.88;
}
.me-wrapper.me-readonly .me-toolbar {
opacity: 0.45; pointer-events: none;
}
/* ============ 高亮标记 ============ */
.me-preview mark {
background: rgba(250, 204, 21, 0.3);
color: inherit;
padding: 0.1em 0.2em;
border-radius: 3px;
}
/* ============ 上标/下标 ============ */
.me-preview sup { font-size: 0.75em; vertical-align: super; line-height: 1; }
.me-preview sub { font-size: 0.75em; vertical-align: sub; line-height: 1; }
/* ============ 数学公式 ============ */
.me-preview .me-math-block {
display: block;
margin: 1.2em 0;
padding: 12px 16px;
background: var(--md-code-bg);
border-radius: 8px;
overflow-x: auto;
font-family: var(--md-mono);
font-size: 0.95em;
text-align: center;
}
.me-preview .me-math-inline {
font-family: var(--md-mono);
font-size: 0.95em;
padding: 0.05em 0.2em;
}
/* ============ 定义列表 ============ */
.me-preview dl { margin: 0.8em 0; }
.me-preview dt { font-weight: 650; margin: 0.6em 0 0.2em; }
.me-preview dd { margin: 0 0 0.3em 1.6em; color: var(--md-text); }
/* ============ 脚注 ============ */
.me-preview .me-footnote-ref a {
font-size: 0.75em;
vertical-align: super;
text-decoration: none;
color: var(--md-accent);
}
.me-preview .me-footnotes {
margin-top: 2em;
border-top: 1px solid var(--md-border);
padding-top: 0.8em;
font-size: 0.9em;
color: var(--md-muted);
}
.me-preview .me-footnotes hr { display: none; }
.me-preview .me-footnotes ol { padding-left: 1.2em; }
.me-preview .me-footnote-item { margin: 0.3em 0; }
.me-preview .me-footnote-backref {
text-decoration: none;
color: var(--md-accent);
margin-right: 0.4em;
}
/* ============ Toast 通知(v0.1.6 ============ */
.me-toast {
position: absolute;
bottom: 16px;
right: 16px;
z-index: 30;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: 8px;
font-size: 13px;
pointer-events: auto;
cursor: pointer;
opacity: 0;
transform: translateY(12px);
transition: opacity 0.3s ease, transform 0.3s ease;
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
max-width: 360px;
}
.me-toast.me-toast-enter { opacity: 1; transform: translateY(0); }
.me-toast.me-toast-leave { opacity: 0; transform: translateY(12px); }
.me-toast-success { background: #10b981; color: #fff; }
.me-toast-error { background: #ef4444; color: #fff; }
.me-toast-warning { background: #f59e0b; color: #fff; }
.me-toast-info { background: var(--md-accent, #3b82f6); color: #fff; }
.me-toast-icon { font-weight: 700; font-size: 16px; flex-shrink: 0; }
.me-toast-msg { line-height: 1.4; }
/* ============ 右键菜单(v0.1.6 ============ */
.me-context-menu {
position: fixed;
z-index: 40;
min-width: 180px;
padding: 4px 0;
background: var(--md-bg);
border: 1px solid var(--md-border);
border-radius: 8px;
box-shadow: 0 8px 28px rgba(0,0,0,0.18);
font-size: 13px;
}
.me-context-menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 14px;
cursor: pointer;
color: var(--md-text);
transition: background 0.1s;
}
.me-context-menu-item:hover { background: var(--md-code-bg); color: var(--md-accent); }
.me-context-menu-sep {
height: 1px;
background: var(--md-border);
margin: 4px 0;
}
.me-context-menu-shortcut {
color: var(--md-muted);
font-size: 11px;
margin-left: 24px;
}
/* ============ 大纲面板(v0.1.7 ============ */
.me-outline {
position: absolute;
top: 0; right: 0;
width: 220px;
height: 100%;
overflow-y: auto;
background: var(--md-preview-bg);
border-left: 1px solid var(--md-border);
padding: 12px 14px;
font-size: 13px;
z-index: 15;
}
.me-outline-title {
font-weight: 650;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid var(--md-border);
color: var(--md-text);
}
.me-outline ul { list-style: none; padding: 0; margin: 0; }
.me-outline li { margin: 2px 0; }
.me-outline a {
color: var(--md-muted);
text-decoration: none;
display: block;
padding: 2px 6px;
border-radius: 4px;
transition: background 0.1s, color 0.1s;
}
.me-outline a:hover { background: var(--md-code-bg); color: var(--md-accent); }
.me-outline-l1 a { font-weight: 600; color: var(--md-text); }
.me-outline-l2 a { padding-left: 12px; }
.me-outline-l3 a { padding-left: 20px; font-size: 12px; }
.me-outline-l4 a { padding-left: 28px; font-size: 12px; }
/* ============ 打印样式 ============ */
@media print {
.me-wrapper { border: 0 !important; box-shadow: none !important; }
.me-toolbar, .me-statusbar, .me-divider { display: none !important; }
.me-body.me-mode-split .me-editor-pane { display: none; }
.me-body.me-mode-split .me-preview-pane { flex: 1 1 100% !important; }
.me-preview { padding: 0 !important; font-size: 11pt; }
.me-wrapper.me-fullscreen { position: static !important; width: auto !important; height: auto !important; }
}
`;
};
/**
* 注入样式(单例)
*/
export const injectStyles = () => {
if (typeof document === 'undefined') return;
if (styleElement && document.getElementById('metona-editor-styles')) return;
const css = generateCSS();
styleElement = document.createElement('style');
styleElement.id = 'metona-editor-styles';
styleElement.textContent = css.replace(/\s+/g, ' ');
document.head.appendChild(styleElement);
};
/**
* 更新样式
*/
export const updateStyles = () => {
if (!styleElement) { injectStyles(); return; }
styleElement.textContent = generateCSS().replace(/\s+/g, ' ');
};
/**
* 移除样式
*/
export const removeStyles = () => {
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
styleElement = null;
};
/**
* 获取系统主题
*/
export const getSystemTheme = () => {
if (typeof window === 'undefined') return 'light';
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
/**
* 监听系统主题变化
*/
export const watchSystemTheme = (callback) => {
if (typeof window === 'undefined' || !window.matchMedia) return () => {};
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e) => callback(e.matches ? 'dark' : 'light');
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
};
export { THEMES };
export default { injectStyles, updateStyles, removeStyles, getSystemTheme, watchSystemTheme };