feat(editor): replace format bar with selection appearance controls
This commit is contained in:
1 parent
19aab631ec
commit
5abc99b89e
15 files changed
+634
-130
No files matched your search
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { HOLLOW, INLINE_FONTS, SHADOW, type InlineStyles } from '../markdown/inlineStyles'
|
||||
import { fixedThemeMode } from '../themes/appearance'
|
||||
import { settings } from './settings'
|
||||
|
||||
const props = defineProps<{ styles: InlineStyles; baseSize: number }>()
|
||||
const emit = defineEmits<{ change: [patch: InlineStyles]; clear: [] }>()
|
||||
const system = matchMedia('(prefers-color-scheme: dark)')
|
||||
const systemDark = ref(system.matches)
|
||||
const onSystem = () => { systemDark.value = system.matches }
|
||||
system.addEventListener('change', onSystem)
|
||||
onBeforeUnmount(() => system.removeEventListener('change', onSystem))
|
||||
const dark = computed(() => (fixedThemeMode(settings.theme) ?? (settings.colorMode === 'system' ? (systemDark.value ? 'dark' : 'light') : settings.colorMode)) === 'dark')
|
||||
// 思源色板顺序;只影响选中文字,不修改主题背景。
|
||||
const foreground = computed(() => dark.value
|
||||
? ['#f39993','#ffd599','#a6d5fa','#b7dfb9','#9aa0a6','#3575f0','#f3a92f','#d23f31','#f47c9c','#4dd0e1','#10df54','#ecd424','#1e1e1e']
|
||||
: ['#790600','#8a4f00','#005599','#006b04','#5f6368','#3575f0','#ff9200','#d23f31','#a40074','#007080','#006e2a','#6b5a00','#ffffff'])
|
||||
const background = computed(() => dark.value
|
||||
? ['#442724','#554636','#28405c','#425347','#3a3f42','#031840','#593905','#3a0c09','#4d1b40','#1a5459','#305415','#4a4712','#dadada']
|
||||
: ['#f5d1cf','#ffe8c8','#d6eaf9','#d7eed8','#e2e3e4','#acd0fc','#fddaab','#ffb0a9','#fdbfff','#b1ffff','#affad1','#fff88f','#222222'])
|
||||
const relative = ref(false)
|
||||
const size = ref(props.baseSize)
|
||||
watch(() => props.styles['font-size'], value => {
|
||||
relative.value = value?.endsWith('em') ?? false
|
||||
size.value = value ? Math.round(parseFloat(value) * (relative.value ? 100 : 1)) : props.baseSize
|
||||
}, { immediate: true })
|
||||
function setSize() {
|
||||
if (!Number.isFinite(size.value) || size.value < (relative.value ? 56 : 9) || size.value > (relative.value ? 450 : 72)) return
|
||||
emit('change', { 'font-size': relative.value ? `${size.value / 100}em` : `${size.value}px` })
|
||||
}
|
||||
function switchSize() {
|
||||
size.value = Math.max(relative.value ? 56 : 9, Math.min(relative.value ? 450 : 72, Math.round(relative.value ? size.value / props.baseSize * 100 : size.value / 100 * props.baseSize)))
|
||||
setSize()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rm-inline-appearance" role="region" aria-label="文字外观">
|
||||
<fieldset><legend>配色</legend><div class="rm-inline-swatches">
|
||||
<button type="button" title="默认配色" aria-label="默认配色" @mousedown.prevent @click="emit('change', { color: undefined, 'background-color': undefined })">A</button>
|
||||
<button v-for="(color, index) in foreground" :key="index" type="button" :title="'配色 ' + (index + 1)" :aria-label="'配色 ' + (index + 1)" :style="{ color, backgroundColor: background[index] }" @mousedown.prevent @click="emit('change', { color, 'background-color': background[index] })">A</button>
|
||||
</div></fieldset>
|
||||
<fieldset v-for="group in [{ key: 'color', title: '字体颜色', colors: foreground }, { key: 'background-color', title: '背景色', colors: background }]" :key="group.key"><legend>{{ group.title }}</legend><div class="rm-inline-swatches">
|
||||
<button type="button" :title="'默认' + group.title" :aria-label="'默认' + group.title" @mousedown.prevent @click="emit('change', { [group.key]: undefined })">↺</button>
|
||||
<button v-for="(color, index) in group.colors" :key="color" type="button" :title="group.title + ' ' + (index + 1)" :aria-label="group.title + ' ' + (index + 1)" :aria-pressed="styles[group.key as keyof InlineStyles] === color" :style="group.key === 'color' ? { color } : { backgroundColor: color, color: foreground[index] }" @mousedown.prevent @click="emit('change', { [group.key]: color })">A</button>
|
||||
<label class="rm-inline-custom" :title="'自定义' + group.title"><span>自定义</span><input type="color" :aria-label="'自定义' + group.title" :value="styles[group.key as keyof InlineStyles] ?? (group.key === 'color' ? '#333333' : '#fff88f')" @change="emit('change', { [group.key]: ($event.target as HTMLInputElement).value })" /></label>
|
||||
</div></fieldset>
|
||||
<fieldset><legend>字体效果</legend><div class="rm-inline-effects">
|
||||
<button type="button" title="空心" aria-label="空心" :aria-pressed="!!styles['-webkit-text-stroke']" :style="{ WebkitTextStroke: HOLLOW, WebkitTextFillColor: 'transparent' }" @mousedown.prevent @click="emit('change', { '-webkit-text-stroke': styles['-webkit-text-stroke'] ? undefined : HOLLOW, '-webkit-text-fill-color': styles['-webkit-text-stroke'] ? undefined : 'transparent' })">A</button>
|
||||
<button type="button" title="投影" aria-label="投影" :aria-pressed="!!styles['text-shadow']" :style="{ textShadow: SHADOW }" @mousedown.prevent @click="emit('change', { 'text-shadow': styles['text-shadow'] ? undefined : SHADOW })">A</button>
|
||||
</div></fieldset>
|
||||
<label class="rm-inline-control">字体<select aria-label="局部字体" :value="styles['font-family'] ?? ''" @change="emit('change', { 'font-family': ($event.target as HTMLSelectElement).value || undefined })"><option value="">继承正文</option><option v-for="font in INLINE_FONTS" :key="font.css" :value="font.css">{{ font.label }}</option></select></label>
|
||||
<div class="rm-inline-control"><span>字号</span><label><input v-model="relative" type="checkbox" aria-label="相对字号" @change="switchSize" /> 相对字号</label><button type="button" class="rm-btn" @mousedown.prevent @click="emit('change', { 'font-size': undefined })">默认</button></div>
|
||||
<form class="rm-inline-size" @submit.prevent="setSize"><input v-model.number="size" type="range" :min="relative ? 56 : 9" :max="relative ? 450 : 72" step="1" aria-label="局部字号滑块" @change="setSize" /><input v-model.number="size" type="number" :min="relative ? 56 : 9" :max="relative ? 450 : 72" step="1" aria-label="局部字号" @change="setSize" /><span>{{ relative ? '%' : 'px' }}</span></form>
|
||||
<button type="button" class="rm-btn rm-inline-clear" @mousedown.prevent @click="emit('clear')">清除样式</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { redo, undo } from '@codemirror/commands'
|
||||
import type { EditorView } from '@codemirror/view'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { cycleHeading, insertLink, toggleBlock, toggleBold, toggleCode, toggleItalic, toggleStrike, toggleInline } from '../editor/format'
|
||||
import Icon, { type IconName } from './Icon.vue'
|
||||
import { settings } from './settings'
|
||||
import { activeTab, onEditorUpdate, ws, editorView, insertImagesFromPicker } from './workspace'
|
||||
|
||||
// 底部格式栏与选区工具条;触屏设备仅在编辑器获得焦点时显示底栏。
|
||||
// 按钮在 mousedown 时阻止默认行为,焦点留在编辑器中,键盘不会收起
|
||||
// (不能在 pointerdown 上阻止:WebKit 会因此不再触发 click)
|
||||
const visible = ref(false)
|
||||
const floatingToolbar = ref<HTMLDivElement>()
|
||||
const floating = ref<{ left: number; top: number } | null>(null)
|
||||
let unsubscribe: (() => void) | undefined
|
||||
const coarse = matchMedia('(pointer: coarse)')
|
||||
|
||||
const FORMAT_BUTTONS: { icon: IconName; title: string; run: (v: EditorView) => unknown }[] = [
|
||||
{ icon: 'task', title: '任务列表', run: toggleBlock('task') },
|
||||
{ icon: 'quote', title: '引用', run: toggleBlock('quote') },
|
||||
{ icon: 'link', title: '链接', run: insertLink },
|
||||
{ icon: 'image', title: '插入图片', run: () => insertImagesFromPicker() },
|
||||
{ icon: 'undo', title: '撤销', run: undo },
|
||||
{ icon: 'redo', title: '重做', run: redo },
|
||||
]
|
||||
|
||||
const SELECTION_BUTTONS: { label: string; title: string; run: (v: EditorView) => unknown; cls?: string }[] = [
|
||||
{ label: 'H', title: '标题', run: cycleHeading, cls: 'bold' },
|
||||
{ label: 'B', title: '粗体', run: toggleBold, cls: 'bold' },
|
||||
{ label: 'I', title: '斜体', run: toggleItalic, cls: 'italic' },
|
||||
{ label: 'S', title: '删除线', run: toggleStrike, cls: 'strike' },
|
||||
{ label: 'M', title: '高亮', run: toggleInline('==') },
|
||||
{ label: '`', title: '行内代码', run: toggleCode, cls: 'mono' },
|
||||
{ label: '↗', title: '链接', run: insertLink },
|
||||
]
|
||||
|
||||
function run(fn: (v: EditorView) => unknown) {
|
||||
const v = editorView()
|
||||
if (v) fn(v)
|
||||
}
|
||||
|
||||
// 焦点从编辑器移到别处时,focusout 先于新元素获得焦点,等一帧再判断
|
||||
function sync() {
|
||||
requestAnimationFrame(async () => {
|
||||
const v = editorView()
|
||||
visible.value = !!ws.active && activeTab()?.status === 'ready' && (!coarse.matches || !!v?.hasFocus)
|
||||
floating.value = null
|
||||
if (!v || coarse.matches || !settings.selectionToolbar || !v.hasFocus || v.state.selection.main.empty || ws.settings || ws.images !== null) return
|
||||
const selection = v.state.selection.main
|
||||
const coords = v.coordsAtPos(selection.head)
|
||||
const rect = v.scrollDOM.getBoundingClientRect()
|
||||
if (!coords || coords.bottom < rect.top || coords.top > rect.bottom) return
|
||||
floating.value = { left: 0, top: 0 }
|
||||
await nextTick()
|
||||
const toolbar = floatingToolbar.value
|
||||
if (!toolbar || !floating.value) return
|
||||
const { width, height } = toolbar.getBoundingClientRect()
|
||||
floating.value = {
|
||||
left: Math.max(12, Math.min(innerWidth - width - 12, coords.left - width / 2)),
|
||||
top: Math.max(8, Math.min(innerHeight - height - 8, coords.top - height - 8 > rect.top ? coords.top - height - 8 : coords.bottom + 8)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
unsubscribe = onEditorUpdate(sync)
|
||||
document.addEventListener('scroll', sync, true)
|
||||
window.addEventListener('resize', sync)
|
||||
sync()
|
||||
document.addEventListener('focusin', sync)
|
||||
document.addEventListener('focusout', sync)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribe?.()
|
||||
document.removeEventListener('scroll', sync, true)
|
||||
window.removeEventListener('resize', sync)
|
||||
document.removeEventListener('focusin', sync)
|
||||
document.removeEventListener('focusout', sync)
|
||||
})
|
||||
watch(() => [ws.active, activeTab()?.status, settings.selectionToolbar, settings.uiFontSize, ws.settings, ws.images], sync)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible" class="rm-format-bar" role="toolbar" aria-label="格式">
|
||||
<div class="rm-format-scroll">
|
||||
<button
|
||||
v-for="b in FORMAT_BUTTONS"
|
||||
:key="b.title"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
:aria-label="b.title"
|
||||
@mousedown.prevent
|
||||
@click="run(b.run)"
|
||||
>
|
||||
<Icon :name="b.icon" class="rm-format-icon" />
|
||||
<span class="rm-format-label" aria-hidden="true">{{ b.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="floatingToolbar" v-if="floating && ws.view === 'notes'" class="rm-selection-toolbar" role="toolbar" aria-label="选中文本格式" :style="{ left: floating.left + 'px', top: floating.top + 'px' }">
|
||||
<button v-for="b in SELECTION_BUTTONS" :key="b.title" :title="b.title" :aria-label="b.title" :class="b.cls" @mousedown.prevent @click="run(b.run)">{{ b.label }}</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import type { EditorSelection } from '@codemirror/state'
|
||||
import { insertLink, type FormatCommand } from '../editor/format'
|
||||
import { applyInlineStyles, canFormatSelection, clearInlineStyles, selectionAppearance, toggleSelectionMark, toggleUnderline } from '../editor/inlineStyles'
|
||||
import type { InlineStyles } from '../markdown/inlineStyles'
|
||||
import Icon from './Icon.vue'
|
||||
import InlineAppearance from './InlineAppearance.vue'
|
||||
import { settings } from './settings'
|
||||
import { activeTab, onEditorUpdate, ws, editorView } from './workspace'
|
||||
|
||||
const root = ref<HTMLElement>()
|
||||
const floating = ref<{ left: number; top: number; maxHeight: number; maxWidth: number } | null>(null)
|
||||
const appearanceOpen = ref(false)
|
||||
const baseSize = ref(settings.fontSize)
|
||||
const appearance = ref<ReturnType<typeof selectionAppearance>>({ styles: {}, underline: false, bold: false, italic: false, strike: false, highlight: false, link: false })
|
||||
let unsubscribe: (() => void) | undefined
|
||||
let frame = 0
|
||||
let dismissed: EditorSelection | undefined
|
||||
const buttons = [
|
||||
{ title: '加粗', label: 'B', cls: 'bold', key: 'bold', run: toggleSelectionMark('StrongEmphasis') },
|
||||
{ title: '斜体', label: 'I', cls: 'italic', key: 'italic', run: toggleSelectionMark('Emphasis') },
|
||||
{ title: '下划线', label: 'U', cls: 'underline', key: 'underline', run: toggleUnderline },
|
||||
{ title: '删除线', label: 'S', cls: 'strike', key: 'strike', run: toggleSelectionMark('Strikethrough') },
|
||||
{ title: '高亮', label: 'M', cls: 'highlight', key: 'highlight', run: toggleSelectionMark('Highlight') },
|
||||
] as const
|
||||
|
||||
function close(restoreFocus = false) {
|
||||
dismissed = editorView()?.state.selection
|
||||
floating.value = null
|
||||
appearanceOpen.value = false
|
||||
if (restoreFocus) editorView()?.focus()
|
||||
}
|
||||
function run(command: FormatCommand, keepPanel = false) {
|
||||
const view = editorView()
|
||||
if (!view || !canFormatSelection(view.state)) return
|
||||
command(view)
|
||||
if (!keepPanel) { appearanceOpen.value = false; view.focus() }
|
||||
sync()
|
||||
}
|
||||
function change(patch: InlineStyles) { run(applyInlineStyles(patch), true) }
|
||||
|
||||
// 保留 CM 选区,允许面板输入框获得焦点;不用 pointerdown.prevent,以免 WebKit 丢 click。
|
||||
function sync() {
|
||||
cancelAnimationFrame(frame)
|
||||
frame = requestAnimationFrame(async () => {
|
||||
const view = editorView()
|
||||
if (view && dismissed && !dismissed.eq(view.state.selection)) dismissed = undefined
|
||||
const inToolbar = !!root.value?.contains(document.activeElement)
|
||||
if (!view || !settings.selectionToolbar || !ws.active || activeTab()?.status !== 'ready' || ws.view !== 'notes' || ws.settings || ws.images !== null || (!view.hasFocus && !inToolbar) || !canFormatSelection(view.state) || dismissed?.eq(view.state.selection)) {
|
||||
floating.value = null
|
||||
appearanceOpen.value = false
|
||||
return
|
||||
}
|
||||
dismissed = undefined
|
||||
const coords = view.coordsAtPos(view.state.selection.main.head)
|
||||
const rect = view.scrollDOM.getBoundingClientRect()
|
||||
if (!coords || coords.bottom < rect.top || coords.top > rect.bottom) { floating.value = null; appearanceOpen.value = false; return }
|
||||
appearance.value = selectionAppearance(view.state)
|
||||
const position = view.domAtPos(view.state.selection.main.from).node
|
||||
const line = (position instanceof Element ? position : position.parentElement)!.closest('.cm-line')!
|
||||
baseSize.value = parseFloat(getComputedStyle(line).fontSize)
|
||||
const viewport = window.visualViewport
|
||||
const left = viewport?.offsetLeft ?? 0, top = viewport?.offsetTop ?? 0
|
||||
const width = viewport?.width ?? innerWidth, height = viewport?.height ?? innerHeight
|
||||
floating.value ??= { left, top, maxHeight: height - 16, maxWidth: width - 16 }
|
||||
floating.value.maxHeight = Math.max(44, height - 16)
|
||||
floating.value.maxWidth = width - 16
|
||||
await nextTick()
|
||||
if (!root.value || !floating.value) return
|
||||
const box = root.value.getBoundingClientRect()
|
||||
const above = coords.top - box.height - 8
|
||||
floating.value = {
|
||||
left: Math.max(left + 8, Math.min(left + width - box.width - 8, coords.left - box.width / 2)),
|
||||
top: Math.max(top + 8, Math.min(top + height - box.height - 8, above >= Math.max(top, rect.top) ? above : coords.bottom + 8)),
|
||||
maxHeight: Math.max(44, height - 16),
|
||||
maxWidth: width - 16,
|
||||
}
|
||||
})
|
||||
}
|
||||
function outside(event: PointerEvent) {
|
||||
if (event.target instanceof Node && root.value?.contains(event.target)) return
|
||||
if (floating.value) close()
|
||||
}
|
||||
function onKey(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && floating.value) { event.preventDefault(); event.stopPropagation(); close(true) }
|
||||
}
|
||||
onMounted(() => {
|
||||
unsubscribe = onEditorUpdate(sync)
|
||||
document.addEventListener('scroll', sync, true)
|
||||
document.addEventListener('selectionchange', sync)
|
||||
document.addEventListener('focusin', sync)
|
||||
document.addEventListener('focusout', sync)
|
||||
document.addEventListener('pointerdown', outside, true)
|
||||
document.addEventListener('keydown', onKey, true)
|
||||
window.addEventListener('resize', sync)
|
||||
window.visualViewport?.addEventListener('resize', sync)
|
||||
window.visualViewport?.addEventListener('scroll', sync)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribe?.()
|
||||
cancelAnimationFrame(frame)
|
||||
document.removeEventListener('scroll', sync, true)
|
||||
document.removeEventListener('selectionchange', sync)
|
||||
document.removeEventListener('focusin', sync)
|
||||
document.removeEventListener('focusout', sync)
|
||||
document.removeEventListener('pointerdown', outside, true)
|
||||
document.removeEventListener('keydown', onKey, true)
|
||||
window.removeEventListener('resize', sync)
|
||||
window.visualViewport?.removeEventListener('resize', sync)
|
||||
window.visualViewport?.removeEventListener('scroll', sync)
|
||||
})
|
||||
watch(() => [ws.active, activeTab()?.status, settings.selectionToolbar, settings.uiFontSize, ws.settings, ws.images, ws.view], () => { dismissed = undefined; sync() })
|
||||
watch(appearanceOpen, sync)
|
||||
watch(() => ws.active, () => { floating.value = null; appearanceOpen.value = false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="floating" ref="root" class="rm-selection-popover" :style="{ left: floating.left + 'px', top: floating.top + 'px', maxHeight: floating.maxHeight + 'px', maxWidth: floating.maxWidth + 'px' }">
|
||||
<div class="rm-selection-toolbar" role="toolbar" aria-label="选中文本格式">
|
||||
<button type="button" title="外观" aria-label="外观" :aria-expanded="appearanceOpen" aria-controls="rm-inline-appearance" @mousedown.prevent @click="appearanceOpen = !appearanceOpen"><Icon name="palette" /></button>
|
||||
<button v-for="button in buttons" :key="button.key" type="button" :title="button.title" :aria-label="button.title" :aria-pressed="appearance[button.key]" :class="button.cls" @mousedown.prevent @click="run(button.run)">{{ button.label }}</button>
|
||||
<button type="button" title="链接" aria-label="链接" :aria-pressed="appearance.link" @mousedown.prevent @click="run(insertLink)"><Icon name="link" /></button>
|
||||
</div>
|
||||
<InlineAppearance v-if="appearanceOpen" id="rm-inline-appearance" :styles="appearance.styles" :base-size="baseSize" @change="change" @clear="run(clearInlineStyles, true)" />
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -7,7 +7,7 @@ import KnowledgePanel from './KnowledgePanel.vue'
|
||||
import TemplateDialog from './TemplateDialog.vue'
|
||||
import EditorChrome from './EditorChrome.vue'
|
||||
import Icon from './Icon.vue'
|
||||
import MobileToolbar from './MobileToolbar.vue'
|
||||
import SelectionToolbar from './SelectionToolbar.vue'
|
||||
import MoveDialog from './MoveDialog.vue'
|
||||
import ImportDialog from './ImportDialog.vue'
|
||||
import ImageGallery from './ImageGallery.vue'
|
||||
@@ -124,7 +124,7 @@ onBeforeUnmount(() => {
|
||||
<EditorChrome />
|
||||
<NoteBanner />
|
||||
<EditorPane />
|
||||
<MobileToolbar />
|
||||
<SelectionToolbar />
|
||||
</div>
|
||||
<KnowledgePanel v-if="ws.view !== 'notes' && ws.images === null" />
|
||||
<ImageGallery v-if="ws.images !== null" :dir="ws.images" />
|
||||
|
||||
+27
-22
@@ -412,24 +412,6 @@ button {
|
||||
.rm-token-created p { margin: 0 0 6px; }
|
||||
.rm-token-value { display: flex; align-items: center; gap: 8px; }
|
||||
.rm-token-value code { flex: 1; min-width: 0; overflow-wrap: anywhere; font-family: var(--rm-font-mono); font-size: var(--rm-ui-font-small); user-select: all; }
|
||||
/* ---- 底部格式工具条 ---- */
|
||||
.rm-format-bar { flex: none; display: flex; justify-content: center; height: var(--rm-control-size); border-top: 1px solid var(--rm-divider); background: var(--rm-sidebar-bg); }
|
||||
.rm-format-scroll { display: flex; align-items: center; }
|
||||
.rm-format-bar button { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: none; width: var(--rm-control-compact); height: var(--rm-control-size); padding: 0; border: 0; border-radius: 5px; background: none; color: var(--rm-text); cursor: pointer; touch-action: manipulation; }
|
||||
.rm-format-bar button:active { background: var(--rm-hover); }
|
||||
.rm-format-icon { transform-origin: center bottom; transition: transform 160ms ease; }
|
||||
.rm-format-label { position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translate(-50%, 4px); padding: 4px 8px; border: 1px solid var(--rm-divider); border-radius: 6px; background: var(--rm-bg); box-shadow: var(--rm-shadow); font-size: var(--rm-ui-font-small); line-height: 1.5; white-space: nowrap; opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 160ms ease, transform 160ms ease, visibility 160ms; }
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.rm-format-bar button:hover .rm-format-icon { transform: translateY(-4px) scale(1.3); }
|
||||
.rm-format-bar button:hover .rm-format-label { opacity: 1; visibility: visible; transform: translate(-50%, 0); }
|
||||
}
|
||||
.rm-format-bar button:focus-visible .rm-format-icon { transform: translateY(-4px) scale(1.3); }
|
||||
.rm-format-bar button:focus-visible .rm-format-label { opacity: 1; visibility: visible; transform: translate(-50%, 0); }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.rm-format-icon, .rm-format-label { transition: none; }
|
||||
.rm-format-bar button:is(:hover, :focus-visible) .rm-format-icon { transform: none; }
|
||||
}
|
||||
|
||||
/* ---- 设置 ---- */
|
||||
.rm-setting { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: var(--rm-control-size); padding: 6px 20px; }
|
||||
.rm-setting-label { font-size: var(--rm-ui-font-size); }
|
||||
@@ -683,10 +665,33 @@ button {
|
||||
.rm-auth-card { padding: 28px 24px; }
|
||||
}
|
||||
.rm-notes-view { display: flex; flex-direction: column; flex: 1; min-height: 0; position: relative; }
|
||||
.rm-selection-toolbar { position: fixed; display: flex; align-items: center; box-sizing: border-box; max-width: calc(100vw - 24px); overflow-x: auto; padding: 4px; border: 1px solid var(--rm-divider); border-radius: 10px; background: color-mix(in srgb, var(--rm-bg) 90%, transparent); backdrop-filter: blur(18px); box-shadow: var(--rm-shadow); z-index: 20; }
|
||||
.rm-selection-toolbar button { width: var(--rm-control-compact); height: var(--rm-control-compact); flex: none; padding: 0; border: 0; border-radius: 6px; background: none; cursor: pointer; font: var(--rm-ui-font-size) var(--rm-font-text); } .rm-selection-toolbar button:hover { background: var(--rm-selection); color: var(--rm-accent); } .rm-selection-toolbar .bold { font-weight: 700; } .rm-selection-toolbar .italic { font-style: italic; } .rm-selection-toolbar .strike { text-decoration: line-through; }
|
||||
@media(min-width:768px) { .rm-format-bar { box-sizing: content-box; position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%); width: max-content; max-width: calc(100% - 18px); padding: 4px; border: 1px solid var(--rm-divider); border-radius: 12px; background: color-mix(in srgb, var(--rm-bg) 85%, transparent); backdrop-filter: blur(18px); box-shadow: var(--rm-shadow); z-index: 15; } }
|
||||
@media(max-width:767px) { .rm-format-bar { padding-bottom: env(safe-area-inset-bottom); } }
|
||||
/* 选区工具栏与局部外观面板 */
|
||||
.rm-selection-popover { position: fixed; display: flex; flex-direction: column; box-sizing: border-box; max-width: calc(100vw - 16px); padding: 4px; border: 1px solid var(--rm-divider); border-radius: 10px; background: color-mix(in srgb, var(--rm-bg) 96%, transparent); color: var(--rm-text); font: var(--rm-ui-font-size)/var(--rm-ui-line-height) var(--rm-font-text); backdrop-filter: blur(18px); box-shadow: var(--rm-shadow); z-index: 20; }
|
||||
.rm-selection-toolbar { display: flex; flex: none; align-items: center; max-width: 100%; overflow-x: auto; }
|
||||
.rm-selection-toolbar > button { display: inline-flex; align-items: center; justify-content: center; width: var(--rm-control-size); height: var(--rm-control-size); flex: none; padding: 0; border: 0; border-radius: 6px; background: none; color: inherit; cursor: pointer; font: inherit; touch-action: manipulation; }
|
||||
.rm-selection-toolbar > button:hover, .rm-selection-toolbar > button[aria-pressed=true], .rm-selection-toolbar > button[aria-expanded=true] { background: var(--rm-selection); color: var(--rm-accent-strong); }
|
||||
.rm-selection-toolbar .bold { font-weight: 700; } .rm-selection-toolbar .italic { font-style: italic; } .rm-selection-toolbar .strike { text-decoration: line-through; } .rm-selection-toolbar .underline { text-decoration: underline; } .rm-selection-toolbar .highlight { text-decoration: underline; text-decoration-color: var(--rm-accent); text-decoration-thickness: 3px; }
|
||||
.rm-inline-appearance { box-sizing: border-box; width: 328px; max-width: 100%; min-height: 0; padding: 4px 8px 8px; overflow-y: auto; overscroll-behavior: contain; border-top: 1px solid var(--rm-divider); }
|
||||
.rm-inline-appearance fieldset { min-width: 0; margin: 8px 0; padding: 0 0 10px; border: 0; border-bottom: 1px solid var(--rm-divider); }
|
||||
.rm-inline-appearance legend { padding: 0 0 6px; color: var(--rm-text-muted); font-size: var(--rm-ui-font-small); }
|
||||
.rm-inline-swatches { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
|
||||
.rm-inline-swatches button, .rm-inline-effects button { box-sizing: border-box; min-height: 28px; padding: 2px; border: 1px solid var(--rm-divider); border-radius: 5px; background: var(--rm-bg); color: var(--rm-text); font: inherit; cursor: pointer; }
|
||||
.rm-inline-swatches button:hover, .rm-inline-effects button:hover, .rm-inline-appearance button[aria-pressed=true] { outline: 2px solid var(--rm-accent); outline-offset: -2px; }
|
||||
.rm-inline-custom { grid-column: span 3; display: flex; align-items: center; justify-content: space-between; gap: 4px; font-size: var(--rm-ui-font-small); }
|
||||
.rm-inline-custom input { box-sizing: border-box; width: 32px; height: 28px; padding: 2px; border: 1px solid var(--rm-divider); border-radius: 4px; background: var(--rm-bg); cursor: pointer; }
|
||||
.rm-inline-effects { display: flex; gap: 8px; } .rm-inline-effects button { width: 36px; font-size: 20px; }
|
||||
.rm-inline-control { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; margin: 10px 0; font-size: var(--rm-ui-font-small); }
|
||||
.rm-inline-control select { width: min(190px, 100%); min-width: 0; }
|
||||
.rm-inline-control select, .rm-inline-size input[type=number] { box-sizing: border-box; min-height: var(--rm-control-compact); padding: 2px 4px; border: 1px solid var(--rm-divider); border-radius: 4px; background: var(--rm-bg); color: inherit; font: inherit; }
|
||||
.rm-inline-size { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; } .rm-inline-size input[type=range] { min-width: 0; width: 100%; flex: 1; accent-color: var(--rm-accent); } .rm-inline-size input[type=number] { width: 64px; } .rm-inline-size > span { min-width: 18px; }
|
||||
.rm-inline-clear { width: 100%; }
|
||||
@media (pointer: coarse) {
|
||||
.rm-selection-toolbar > button { width: 44px; height: 44px; }
|
||||
.rm-inline-swatches { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||
.rm-inline-effects button, .rm-inline-custom input { min-width: 44px; }
|
||||
.rm-inline-swatches button, .rm-inline-effects button, .rm-inline-appearance select, .rm-inline-appearance input, .rm-inline-appearance .rm-btn { min-height: 44px; }
|
||||
.rm-inline-appearance select, .rm-inline-size input[type=number] { font-size: max(16px, var(--rm-ui-font-size)); }
|
||||
}
|
||||
|
||||
.rm-shell { position: relative; }
|
||||
@media(min-width:768px) { .rm-shell:has(.rm-media-library) .rm-sidebar { width: 0; border-right-width: 0; visibility: hidden; pointer-events: none; transition-delay: 0s, 0s, 0s, var(--rm-panel-duration); } }
|
||||
|
||||
Reference in new issue
Block a user