feat(ui): enhance theme toggle styling and add Chinese localization

- Redesign theme toggle with improved visual effects including gradient overlays, enhanced shadows, and smoother cubic-bezier transitions
- Update toggle dimensions and icon positioning for better visual balance
- Add SVG filter effects for sun/moon icons in dark mode
- Replace English UI text with Chinese localization in MilkdownEditor
- Refactor copilotPlugin by removing unused decoration functions and improving ghost mark text node handling
This commit is contained in:
2026-02-15 18:24:34 +08:00
parent 838eec30a8
commit 190bb2b756
3 changed files with 76 additions and 59 deletions
+53 -23
View File
@@ -74,63 +74,93 @@ function onChange(markdownValue) {
.theme-toggle__track { .theme-toggle__track {
position: relative; position: relative;
width: 78px; width: 72px;
height: 40px; height: 36px;
display: block; display: block;
border-radius: 999px; border-radius: 999px;
border: 1px solid var(--panel-border); border: 2px solid var(--panel-border);
background: linear-gradient(135deg, var(--toggle-bg-start), var(--toggle-bg-end)); background: linear-gradient(135deg, var(--toggle-bg-start), var(--toggle-bg-end));
box-shadow: var(--panel-shadow); box-shadow: var(--panel-shadow), inset 0 2px 4px rgba(255, 255, 255, 0.1), inset 0 -2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: background 400ms ease, border-color 400ms ease, box-shadow 400ms ease;
}
.theme-toggle__track::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(255,255,255,0.15) 0%, transparent 50%, rgba(0,0,0,0.1) 100%);
border-radius: 999px;
pointer-events: none;
transition: opacity 400ms ease;
} }
.theme-toggle__thumb { .theme-toggle__thumb {
position: absolute; position: absolute;
top: 3px; top: 2px;
left: 3px; left: 2px;
width: 32px; width: 28px;
height: 32px; height: 28px;
border-radius: 50%; border-radius: 50%;
background: var(--toggle-thumb-bg); background: linear-gradient(135deg, var(--toggle-thumb-bg) 0%, var(--toggle-thumb-bg) 100%);
box-shadow: 0 5px 14px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.4);
transition: transform 220ms ease; transition: transform 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 2;
} }
.theme-toggle__sun, .theme-toggle__sun,
.theme-toggle__moon { .theme-toggle__moon {
position: absolute; position: absolute;
top: 50%; top: 50%;
width: 16px; width: 18px;
height: 16px; height: 18px;
transform: translateY(-50%); transform: translateY(-50%);
transition: opacity 200ms ease, transform 220ms ease; transition: opacity 300ms ease, transform 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
} }
.theme-toggle__sun { .theme-toggle__sun {
left: 12px; left: 10px;
color: var(--toggle-sun); color: var(--toggle-sun);
opacity: 1; opacity: 1;
transform: translateY(-50%) rotate(0deg); transform: translateY(-50%) scale(1);
}
.theme-toggle__sun svg {
width: 18px;
height: 18px;
filter: drop-shadow(0 0 3px var(--toggle-sun));
} }
.theme-toggle__moon { .theme-toggle__moon {
right: 12px; right: 10px;
color: var(--toggle-moon); color: var(--toggle-moon);
opacity: 0.55; opacity: 0.5;
transform: translateY(-50%) rotate(-16deg); transform: translateY(-50%) scale(0.85);
}
.theme-toggle__moon svg {
width: 18px;
height: 18px;
} }
.theme-toggle.is-dark .theme-toggle__thumb { .theme-toggle.is-dark .theme-toggle__thumb {
transform: translateX(38px); transform: translateX(36px);
background: linear-gradient(135deg, var(--toggle-thumb-bg) 0%, #c8d4ec 100%);
} }
.theme-toggle.is-dark .theme-toggle__sun { .theme-toggle.is-dark .theme-toggle__sun {
opacity: 0.55; opacity: 0.5;
transform: translateY(-50%) rotate(16deg); transform: translateY(-50%) scale(0.85);
} }
.theme-toggle.is-dark .theme-toggle__moon { .theme-toggle.is-dark .theme-toggle__moon {
opacity: 1; opacity: 1;
transform: translateY(-50%) rotate(0deg); transform: translateY(-50%) scale(1);
color: #c8d4ec;
}
.theme-toggle.is-dark .theme-toggle__moon svg {
filter: drop-shadow(0 0 4px rgba(200, 212, 236, 0.6));
} }
.theme-toggle:focus-visible { .theme-toggle:focus-visible {
+17 -12
View File
@@ -38,8 +38,8 @@
<button <button
type="button" type="button"
class="action-btn" class="action-btn"
aria-label="Insert Image" aria-label="上传图片"
title="Insert Image" title="上传图片"
@click="toggleImageDropdown" @click="toggleImageDropdown"
> >
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -47,11 +47,11 @@
<circle cx="8.5" cy="8.5" r="1.5"/> <circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/> <polyline points="21 15 16 10 5 21"/>
</svg> </svg>
<span class="btn-tooltip">Insert Image</span> <span class="btn-tooltip">上传图片</span>
</button> </button>
<div v-if="showImageDropdown" class="image-dropdown"> <div v-if="showImageDropdown" class="image-dropdown">
<button type="button" @click="triggerImageUpload">Upload Local Image</button> <button type="button" @click="triggerImageUpload">上传本地图片</button>
<button type="button" @click="showUrlDialog = true; showImageDropdown = false">Insert from URL</button> <button type="button" @click="showUrlDialog = true; showImageDropdown = false">通过 URL 插入</button>
</div> </div>
</div> </div>
<input type="file" ref="imageInputRef" @change="handleImageUpload" accept="image/*" style="display:none"> <input type="file" ref="imageInputRef" @change="handleImageUpload" accept="image/*" style="display:none">
@@ -65,9 +65,14 @@
:aria-label="aiButtonLabel" :aria-label="aiButtonLabel"
:title="aiButtonLabel" :title="aiButtonLabel"
> >
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg v-if="aiEnabled && !isOverLimit" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2z"/> <path d="M12 3l1.9 3.9L18 9l-4.1 2.1L12 15l-1.9-3.9L6 9l4.1-2.1L12 3z"/>
<path d="M12 6v6l4 2"/> <path d="M5 14l.9 1.8L8 16.7l-2.1.9L5 20l-.9-2.4L2 16.7l2.1-.9L5 14z"/>
<path d="M19 14l.6 1.2 1.4.6-1.4.6L19 18l-.6-1.6-1.4-.6 1.4-.6L19 14z"/>
</svg>
<svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="9"/>
<line x1="5" y1="5" x2="19" y2="19"/>
</svg> </svg>
<span class="btn-tooltip">{{ aiButtonLabel }}</span> <span class="btn-tooltip">{{ aiButtonLabel }}</span>
</button> </button>
@@ -79,16 +84,16 @@
<div v-if="showUrlDialog" class="url-dialog-overlay" @click.self="showUrlDialog = false"> <div v-if="showUrlDialog" class="url-dialog-overlay" @click.self="showUrlDialog = false">
<div class="url-dialog"> <div class="url-dialog">
<h3>Insert Image from URL</h3> <h3>通过 URL 插入图片</h3>
<input <input
v-model="imageUrl" v-model="imageUrl"
type="url" type="url"
placeholder="Enter image URL" placeholder="请输入图片 URL"
@keyup.enter="insertImageFromUrl" @keyup.enter="insertImageFromUrl"
/> />
<div class="url-dialog-buttons"> <div class="url-dialog-buttons">
<button type="button" class="dialog-btn primary" @click="insertImageFromUrl">Insert</button> <button type="button" class="dialog-btn primary" @click="insertImageFromUrl">插入</button>
<button type="button" class="dialog-btn" @click="showUrlDialog = false; imageUrl = ''">Cancel</button> <button type="button" class="dialog-btn" @click="showUrlDialog = false; imageUrl = ''">取消</button>
</div> </div>
</div> </div>
</div> </div>
+6 -24
View File
@@ -3,7 +3,6 @@ import { $prose, $ctx, $markSchema } from '@milkdown/kit/utils'
import { parserCtx, serializerCtx } from '@milkdown/kit/core' import { parserCtx, serializerCtx } from '@milkdown/kit/core'
import { Node as ProseNode, DOMParser, DOMSerializer } from '@milkdown/prose/model' import { Node as ProseNode, DOMParser, DOMSerializer } from '@milkdown/prose/model'
import type { Ctx } from '@milkdown/kit/core' import type { Ctx } from '@milkdown/kit/core'
import { Decoration, DecorationSet } from '@milkdown/prose/view'
import type { EditorView } from '@milkdown/prose/view' import type { EditorView } from '@milkdown/prose/view'
import { getOcrCache, OCR_SIZE_LIMIT, extractTextFromOCR } from '../utils/ocrCache' import { getOcrCache, OCR_SIZE_LIMIT, extractTextFromOCR } from '../utils/ocrCache'
@@ -119,26 +118,6 @@ function clearGhostText(view: EditorView): boolean {
return true return true
} }
function buildGhostBlockDecorations(state: any): DecorationSet | null {
const pluginState = COPILOT_PLUGIN_KEY.getState(state) as CopilotState | undefined
if (!pluginState || !pluginState.suggestion || pluginState.from >= pluginState.to) {
return null
}
const from = Math.max(0, Math.min(pluginState.from, state.doc.content.size))
const to = Math.max(from, Math.min(pluginState.to, state.doc.content.size))
const decorations: Decoration[] = []
state.doc.nodesBetween(from, to, (node: any, pos: number) => {
if (!node.isBlock || node.nodeSize <= 0) return true
decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: 'copilot-ghost-block' }))
return true
})
if (decorations.length === 0) return null
return DecorationSet.create(state.doc, decorations)
}
function getCursorBeforeGhostInsert(tr: any, from: number): number { function getCursorBeforeGhostInsert(tr: any, from: number): number {
const mapped = tr.mapping.map(from, -1) const mapped = tr.mapping.map(from, -1)
return Math.max(0, Math.min(mapped, tr.doc.content.size)) return Math.max(0, Math.min(mapped, tr.doc.content.size))
@@ -167,9 +146,13 @@ function addGhostMarksToTextNodes(tr: any, from: number, to: number, markType: a
tr.doc.nodesBetween(from, to, (node: any, pos: number) => { tr.doc.nodesBetween(from, to, (node: any, pos: number) => {
if (!node.isText || node.nodeSize <= 0) return true if (!node.isText || node.nodeSize <= 0) return true
const $pos = tr.doc.resolve(pos) const start = Math.max(pos, from)
const end = Math.min(pos + node.nodeSize, to)
if (end <= start) return true
const $pos = tr.doc.resolve(start)
if ($pos.parent.type.allowsMarkType?.(markType)) { if ($pos.parent.type.allowsMarkType?.(markType)) {
tr.addMark(pos, pos + node.nodeSize, markType.create()) tr.addMark(start, end, markType.create())
} }
return true return true
}) })
@@ -454,7 +437,6 @@ export const copilotPlugin = $prose((ctx) => new Plugin<CopilotState>({
} }
}, },
props: { props: {
decorations: (state) => buildGhostBlockDecorations(state),
handleKeyDown: (view, event) => { handleKeyDown: (view, event) => {
const hasGhost = hasGhostText(view) const hasGhost = hasGhostText(view)