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:
76
src/App.vue
76
src/App.vue
@@ -74,63 +74,93 @@ function onChange(markdownValue) {
|
||||
|
||||
.theme-toggle__track {
|
||||
position: relative;
|
||||
width: 78px;
|
||||
height: 40px;
|
||||
width: 72px;
|
||||
height: 36px;
|
||||
display: block;
|
||||
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));
|
||||
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 {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--toggle-thumb-bg);
|
||||
box-shadow: 0 5px 14px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 220ms ease;
|
||||
background: linear-gradient(135deg, var(--toggle-thumb-bg) 0%, var(--toggle-thumb-bg) 100%);
|
||||
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 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.theme-toggle__sun,
|
||||
.theme-toggle__moon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
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 {
|
||||
left: 12px;
|
||||
left: 10px;
|
||||
color: var(--toggle-sun);
|
||||
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 {
|
||||
right: 12px;
|
||||
right: 10px;
|
||||
color: var(--toggle-moon);
|
||||
opacity: 0.55;
|
||||
transform: translateY(-50%) rotate(-16deg);
|
||||
opacity: 0.5;
|
||||
transform: translateY(-50%) scale(0.85);
|
||||
}
|
||||
|
||||
.theme-toggle__moon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
opacity: 0.55;
|
||||
transform: translateY(-50%) rotate(16deg);
|
||||
opacity: 0.5;
|
||||
transform: translateY(-50%) scale(0.85);
|
||||
}
|
||||
|
||||
.theme-toggle.is-dark .theme-toggle__moon {
|
||||
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 {
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
<button
|
||||
type="button"
|
||||
class="action-btn"
|
||||
aria-label="Insert Image"
|
||||
title="Insert Image"
|
||||
aria-label="上传图片"
|
||||
title="上传图片"
|
||||
@click="toggleImageDropdown"
|
||||
>
|
||||
<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"/>
|
||||
<polyline points="21 15 16 10 5 21"/>
|
||||
</svg>
|
||||
<span class="btn-tooltip">Insert Image</span>
|
||||
<span class="btn-tooltip">上传图片</span>
|
||||
</button>
|
||||
<div v-if="showImageDropdown" class="image-dropdown">
|
||||
<button type="button" @click="triggerImageUpload">Upload Local Image</button>
|
||||
<button type="button" @click="showUrlDialog = true; showImageDropdown = false">Insert from URL</button>
|
||||
<button type="button" @click="triggerImageUpload">上传本地图片</button>
|
||||
<button type="button" @click="showUrlDialog = true; showImageDropdown = false">通过 URL 插入</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" ref="imageInputRef" @change="handleImageUpload" accept="image/*" style="display:none">
|
||||
@@ -65,9 +65,14 @@
|
||||
:aria-label="aiButtonLabel"
|
||||
:title="aiButtonLabel"
|
||||
>
|
||||
<svg 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 6v6l4 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 3l1.9 3.9L18 9l-4.1 2.1L12 15l-1.9-3.9L6 9l4.1-2.1L12 3z"/>
|
||||
<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>
|
||||
<span class="btn-tooltip">{{ aiButtonLabel }}</span>
|
||||
</button>
|
||||
@@ -79,16 +84,16 @@
|
||||
|
||||
<div v-if="showUrlDialog" class="url-dialog-overlay" @click.self="showUrlDialog = false">
|
||||
<div class="url-dialog">
|
||||
<h3>Insert Image from URL</h3>
|
||||
<h3>通过 URL 插入图片</h3>
|
||||
<input
|
||||
v-model="imageUrl"
|
||||
type="url"
|
||||
placeholder="Enter image URL"
|
||||
placeholder="请输入图片 URL"
|
||||
@keyup.enter="insertImageFromUrl"
|
||||
/>
|
||||
<div class="url-dialog-buttons">
|
||||
<button type="button" class="dialog-btn primary" @click="insertImageFromUrl">Insert</button>
|
||||
<button type="button" class="dialog-btn" @click="showUrlDialog = false; imageUrl = ''">Cancel</button>
|
||||
<button type="button" class="dialog-btn primary" @click="insertImageFromUrl">插入</button>
|
||||
<button type="button" class="dialog-btn" @click="showUrlDialog = false; imageUrl = ''">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { $prose, $ctx, $markSchema } from '@milkdown/kit/utils'
|
||||
import { parserCtx, serializerCtx } from '@milkdown/kit/core'
|
||||
import { Node as ProseNode, DOMParser, DOMSerializer } from '@milkdown/prose/model'
|
||||
import type { Ctx } from '@milkdown/kit/core'
|
||||
import { Decoration, DecorationSet } from '@milkdown/prose/view'
|
||||
import type { EditorView } from '@milkdown/prose/view'
|
||||
import { getOcrCache, OCR_SIZE_LIMIT, extractTextFromOCR } from '../utils/ocrCache'
|
||||
|
||||
@@ -119,26 +118,6 @@ function clearGhostText(view: EditorView): boolean {
|
||||
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 {
|
||||
const mapped = tr.mapping.map(from, -1)
|
||||
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) => {
|
||||
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)) {
|
||||
tr.addMark(pos, pos + node.nodeSize, markType.create())
|
||||
tr.addMark(start, end, markType.create())
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -454,7 +437,6 @@ export const copilotPlugin = $prose((ctx) => new Plugin<CopilotState>({
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations: (state) => buildGhostBlockDecorations(state),
|
||||
handleKeyDown: (view, event) => {
|
||||
const hasGhost = hasGhostText(view)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user