feat(web): add code copy button, block-scoped select-all, table toolbar polish

- Copy button in the top-right of fenced code blocks, with execCommand
  fallback when the Clipboard API is unavailable over plain HTTP
- Mod-A inside a code block selects the block content first; pressing
  it again selects the whole document
- Paint code block backgrounds on a ::before layer below CodeMirror's
  selection layer so selections inside code blocks are visible
- Table toolbar sits in reserved space above the table instead of
  overlapping it, with grouped icon buttons
This commit is contained in:
eezd committed 2026-10-16 18:07:42 +08:00
1 parent 54fecf71c8
commit 63f508d3df
5 files changed
+218 -59

No files matched your search

+74 -30
View File
@@ -99,26 +99,52 @@ body {
border-radius: 4px;
}
/* 代码块 */
/* 代码块
* 背景画在 ::before 上并置于选区图层之下:若直接给 .cm-line 设背景,会盖住 CodeMirror 绘制的选区 */
.cm-line.cm-rm-code {
position: relative;
font-family: var(--rm-font-mono);
font-size: var(--rm-code-font-size);
line-height: 1.5;
background: var(--rm-code-bg);
color: var(--rm-code-text);
padding: 0 24px;
tab-size: 4;
}
.cm-line.cm-rm-code .cm-rm-mono { font-family: inherit; }
.cm-line.cm-rm-code-begin { border-radius: 8px 8px 0 0; padding-top: 10px; }
.cm-line.cm-rm-code-end { border-radius: 0 0 8px 8px; padding-bottom: 10px; }
.cm-line.cm-rm-code-begin.cm-rm-code-end { border-radius: 8px; }
/* 未激活时的围栏行:压低行高,只显示右上角语言标签 */
.cm-line.cm-rm-code-fence { font-size: 12px; line-height: 1.2; text-align: right; }
.cm-rm-code-lang {
color: rgba(235, 235, 235, 0.38);
user-select: none;
.cm-line.cm-rm-code::before {
content: '';
position: absolute;
inset: 0;
background: var(--rm-code-bg);
z-index: -10;
pointer-events: none;
}
.cm-line.cm-rm-code .cm-rm-mono { font-family: inherit; }
.cm-line.cm-rm-code-begin { padding-top: 10px; }
.cm-line.cm-rm-code-end { padding-bottom: 10px; }
.cm-line.cm-rm-code-begin::before { border-radius: 8px 8px 0 0; }
.cm-line.cm-rm-code-end::before { border-radius: 0 0 8px 8px; }
.cm-line.cm-rm-code-begin.cm-rm-code-end::before { border-radius: 8px; }
/* 未激活时的围栏行:压低行高,只显示右上角语言标签与复制按钮 */
.cm-line.cm-rm-code-fence { font-size: 12px; line-height: 1.2; text-align: right; padding-right: 12px; }
.cm-rm-code-header { display: inline-flex; align-items: center; gap: 6px; vertical-align: middle; user-select: none; }
.cm-line.cm-rm-code-begin:not(.cm-rm-code-fence) .cm-rm-code-header { position: absolute; top: 6px; right: 12px; }
.cm-rm-code-lang { color: rgba(235, 235, 235, 0.38); font-size: 12px; line-height: 1; }
.cm-rm-code-copy {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
border: 0;
border-radius: 6px;
background: transparent;
color: rgba(235, 235, 235, 0.45);
cursor: pointer;
transition: color 0.15s, background-color 0.15s;
}
.cm-rm-code-copy:hover { color: #fff; background: rgba(255, 255, 255, 0.08); }
.cm-rm-code-copy.cm-rm-copied { color: #42d392; }
.cm-rm-code .cm-rm-markup { color: rgba(235, 235, 235, 0.38); }
/* 引用 */
@@ -142,7 +168,7 @@ body {
/* 表格:光标在表格内时以等宽源码显示,否则为可编辑的渲染表格 */
.cm-line.cm-rm-table { font-family: var(--rm-font-mono); font-size: 0.9em; }
.cm-rm-table-widget { position: relative; padding: 4px 0; width: fit-content; max-width: 100%; }
.cm-rm-table-widget { padding: 2px 0 4px; width: fit-content; max-width: 100%; }
.cm-rm-table-scroll { overflow-x: auto; }
.cm-rm-table-widget table { border-collapse: collapse; font-size: 0.95em; line-height: 1.5; }
.cm-rm-table-widget th,
@@ -157,33 +183,51 @@ body {
.cm-rm-table-widget [contenteditable] { box-shadow: inset 0 0 0 2px var(--rm-accent); font-family: var(--rm-font-mono); font-size: 0.9em; }
.cm-rm-table-widget code { font-family: var(--rm-font-mono); font-size: 0.875em; background: var(--rm-inline-code-bg); color: var(--rm-inline-code-text); padding: 0.1em 0.35em; border-radius: 4px; }
.cm-rm-table-widget a { color: var(--rm-accent-strong); }
/* 工具栏占据表格上方的固定空间,只切换可见性,不遮挡表格也不引起跳动 */
.cm-rm-table-toolbar {
position: absolute;
top: -22px;
right: 0;
display: none;
gap: 2px;
font-size: 12px;
display: flex;
justify-content: flex-end;
margin-bottom: 6px;
visibility: hidden;
opacity: 0;
transition: opacity 0.15s;
}
.cm-rm-table-widget:hover .cm-rm-table-toolbar,
.cm-rm-table-widget:focus-within .cm-rm-table-toolbar { display: flex; }
.cm-rm-table-toolbar button {
font: inherit;
padding: 2px 8px;
min-height: 22px;
.cm-rm-table-widget:focus-within .cm-rm-table-toolbar { visibility: visible; opacity: 1; }
.cm-rm-table-tools {
display: inline-flex;
align-items: center;
padding: 2px;
border: 1px solid var(--rm-divider);
border-radius: 4px;
border-radius: 8px;
background: var(--rm-bg);
color: var(--rm-text-muted);
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.cm-rm-table-toolbar button:hover { color: var(--rm-accent-strong); border-color: var(--rm-accent); }
.cm-rm-table-tools button {
display: inline-flex;
align-items: center;
gap: 4px;
height: 26px;
padding: 0 8px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--rm-text-muted);
font: 12px/1 var(--rm-font-text);
cursor: pointer;
transition: color 0.15s, background-color 0.15s;
}
.cm-rm-table-tools button:hover { color: var(--rm-accent-strong); background: var(--rm-selection); }
.cm-rm-table-tools button.cm-rm-danger:hover { color: #e5484d; background: rgba(229, 72, 77, 0.1); }
.cm-rm-table-tools svg { flex: none; }
.cm-rm-table-tools .cm-rm-sep { width: 1px; height: 16px; margin: 0 2px; background: var(--rm-divider); }
.cm-rm-image { cursor: pointer; }
@media (max-width: 640px) {
.cm-editor .cm-content { padding: 16px 16px 40vh; }
/* 触屏没有悬停,工具栏常显 */
.cm-rm-table-toolbar { display: flex; position: static; justify-content: flex-end; margin-bottom: 4px; }
.cm-rm-table-toolbar button { min-height: 32px; padding: 4px 10px; }
/* 触屏没有悬停,工具栏常显,按钮加大触控热区 */
.cm-rm-table-toolbar { visibility: visible; opacity: 1; }
.cm-rm-table-tools button { height: 36px; padding: 0 10px; }
.cm-rm-code-copy { width: 36px; height: 36px; }
.cm-line.cm-rm-code { padding: 0 12px; }
}