feat(editor): replace format bar with selection appearance controls

This commit is contained in:
eezd committed 2026-10-24 18:11:57 +08:00
1 parent 19aab631ec
commit 5abc99b89e
15 files changed
+634 -130

No files matched your search

+58
View File
@@ -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>
-104
View File
@@ -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>
+129
View File
@@ -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>
+2 -2
View File
@@ -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
View File
@@ -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); } }