新增: 批注/备注/对齐分布/形状扩充/video 元素——编辑器能力增强
This commit is contained in:
@@ -7,9 +7,10 @@ import { store } from '../../core/store'
|
||||
import { elementTypes } from '../../core/sample'
|
||||
import { generateImage, isImageConfigured, checkImageQuota } from '../../core/ai'
|
||||
import { readAsDataURL } from '../../core/importer'
|
||||
import { appAlert, appConfirm, appPrompt } from '../../core/dialog'
|
||||
import { markdownToSegments, segmentsToPlain, hasFormatting } from '../../core/richtext'
|
||||
import AddGrid from './AddGrid.vue'
|
||||
import type { ElementType, ChartType } from '../../core/types'
|
||||
import type { ElementType, ChartType, ShapeType } from '../../core/types'
|
||||
|
||||
const CHART_TYPES: Array<{ k: ChartType; label: string; icon: string }> = [
|
||||
{ k: 'bar', label: '柱状图', icon: '📊' },
|
||||
@@ -31,7 +32,7 @@ const content = ref('')
|
||||
const fontSize = ref(24)
|
||||
const colorSel = ref('primary')
|
||||
const colorPicker = ref('#000000')
|
||||
const shape = ref<'rect' | 'circle' | 'triangle'>('rect')
|
||||
const shape = ref<ShapeType>('rect')
|
||||
const chartType = ref<ChartType>('bar')
|
||||
const codeLang = ref('')
|
||||
const imgBusy = ref(false)
|
||||
@@ -66,11 +67,11 @@ watch(selected, (el) => {
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const hasText = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula'].includes(selected.value.type))
|
||||
const canFont = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula'].includes(selected.value.type))
|
||||
const canAlign = computed(() => selected.value && ['title', 'text', 'quote', 'stat', 'table', 'formula'].includes(selected.value.type))
|
||||
const canBI = computed(() => selected.value && ['title', 'text', 'quote', 'list'].includes(selected.value.type))
|
||||
const canColor = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula'].includes(selected.value.type))
|
||||
const hasText = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula', 'shape', 'card'].includes(selected.value.type))
|
||||
const canFont = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula', 'shape', 'card'].includes(selected.value.type))
|
||||
const canAlign = computed(() => selected.value && ['title', 'text', 'quote', 'stat', 'table', 'formula', 'shape'].includes(selected.value.type))
|
||||
const canBI = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'shape'].includes(selected.value.type))
|
||||
const canColor = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula', 'shape'].includes(selected.value.type))
|
||||
const isShape = computed(() => selected.value?.type === 'shape')
|
||||
|
||||
const typeLabel = computed(() => {
|
||||
@@ -87,9 +88,20 @@ function onMultiCopy() {
|
||||
const sel = store.getSelection()
|
||||
if (sel.length) store.copyElements(sel)
|
||||
}
|
||||
function onMultiDel() {
|
||||
async function onMultiDel() {
|
||||
const sel = store.getSelection()
|
||||
if (sel.length) store.delElements(sel)
|
||||
if (!sel.length) return
|
||||
if (await appConfirm('删除选中的 ' + sel.length + ' 个元素?', '可用 Ctrl+Z 撤销', { danger: true, okText: '删除' })) {
|
||||
store.delElements(sel)
|
||||
}
|
||||
}
|
||||
function onAlignOp(edge: 'left' | 'hcenter' | 'right' | 'top' | 'vmiddle' | 'bottom') {
|
||||
const sel = store.getSelection()
|
||||
if (sel.length > 1) store.alignElements(sel, edge)
|
||||
}
|
||||
function onDistribute(axis: 'h' | 'v') {
|
||||
const sel = store.getSelection()
|
||||
if (sel.length > 2) store.distributeElements(sel, axis)
|
||||
}
|
||||
|
||||
function onContentInput() {
|
||||
@@ -152,9 +164,12 @@ function onZ(dir: number) {
|
||||
if (!selected.value) return
|
||||
store.moveElementZ(selected.value.id, dir)
|
||||
}
|
||||
function onDel() {
|
||||
async function onDel() {
|
||||
if (!selected.value) return
|
||||
store.delElement(selected.value.id)
|
||||
const label = elementTypes[selected.value.type]?.label || '元素'
|
||||
if (await appConfirm('删除这个' + label + '?', '可用 Ctrl+Z 撤销', { danger: true, okText: '删除' })) {
|
||||
store.delElement(selected.value.id)
|
||||
}
|
||||
}
|
||||
function onBg(k: string) {
|
||||
store.setSlideBackground(k)
|
||||
@@ -189,23 +204,62 @@ async function onAiImage() {
|
||||
if (imgBusy.value) return
|
||||
const el = selected.value
|
||||
if (!el || el.type !== 'image') return
|
||||
if (!isImageConfigured()) { alert('请先在「AI 设置」中配置 API Key'); return }
|
||||
const promptText = window.prompt('描述你想要的图片,例如「现代办公室协作场景,俯拍,柔和光线」')
|
||||
if (!isImageConfigured()) { appAlert('需要 API Key', '请先在「AI 设置」中配置'); return }
|
||||
const promptText = await appPrompt('AI 配图', { placeholder: '描述你想要的图片,如「现代办公室协作场景,俯拍,柔和光线」' })
|
||||
if (!promptText) return
|
||||
imgBusy.value = true
|
||||
imgAbort = new AbortController()
|
||||
try {
|
||||
const r = await generateImage({ prompt: promptText, signal: imgAbort.signal })
|
||||
const quotaErr = checkImageQuota(r.url)
|
||||
if (quotaErr) { alert(quotaErr); return }
|
||||
if (quotaErr) { appAlert('配图受限', quotaErr); return }
|
||||
store.updateElement(el.id, { content: r.url })
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') alert('配图失败:' + (e?.message || String(e)))
|
||||
if (e?.name !== 'AbortError') appAlert('配图失败', e?.message || String(e))
|
||||
} finally {
|
||||
imgBusy.value = false; imgAbort = null
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 批注(仅图片元素) ---------- */
|
||||
/** 复用文件内颜色选项集合(与颜色 select 的 option values 同款) */
|
||||
const ANNO_COLORS: Array<{ k: string; label: string }> = [
|
||||
{ k: 'primary', label: '主色' },
|
||||
{ k: 'accent', label: '强调色' },
|
||||
{ k: 'text', label: '正文色' },
|
||||
{ k: 'muted', label: '次要色' },
|
||||
{ k: '#ffffff', label: '白色' }
|
||||
]
|
||||
|
||||
/** 当前图片的批注列表 */
|
||||
const annotations = computed<any[]>(() => selected.value?.style?.annotations || [])
|
||||
|
||||
/** 折叠状态:记录「已展开」的批注 id(默认折叠,仅展开正在编辑的一条) */
|
||||
const expandedAnno = ref<string | null>(null)
|
||||
function toggleAnno(annoId: string) {
|
||||
expandedAnno.value = expandedAnno.value === annoId ? null : annoId
|
||||
}
|
||||
|
||||
function addAnno() {
|
||||
if (!selected.value) return
|
||||
const id = store.addAnnotation(selected.value.id)
|
||||
if (id) expandedAnno.value = id // 新增自动展开
|
||||
}
|
||||
function delAnno(annoId: string) {
|
||||
if (selected.value) store.delAnnotation(selected.value.id, annoId)
|
||||
if (expandedAnno.value === annoId) expandedAnno.value = null
|
||||
}
|
||||
function patchAnno(annoId: string, patch: Record<string, any>) {
|
||||
if (!selected.value) return
|
||||
store.updateAnnotation(selected.value.id, annoId, patch)
|
||||
}
|
||||
|
||||
/** 批注文字摘要(折叠态卡片头显示) */
|
||||
function annoSummary(anno: any): string {
|
||||
const t = (anno.text || '').trim()
|
||||
return t ? (t.length > 12 ? t.slice(0, 12) + '…' : t) : '(空批注)'
|
||||
}
|
||||
|
||||
/** 本地上传图片替换当前图片元素内容 */
|
||||
function onLocalImage() {
|
||||
const el = selected.value
|
||||
@@ -219,14 +273,43 @@ function onLocalImage() {
|
||||
try {
|
||||
const dataUrl = await readAsDataURL(file)
|
||||
const quotaErr = checkImageQuota(dataUrl)
|
||||
if (quotaErr) { alert(quotaErr); return }
|
||||
if (quotaErr) { appAlert('图片受限', quotaErr); return }
|
||||
store.updateElement(el.id, { content: dataUrl })
|
||||
} catch (e: any) {
|
||||
alert('读取图片失败:' + (e?.message || String(e)))
|
||||
appAlert('读取图片失败', e?.message || String(e))
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
/* ---------- 视频元素 ---------- */
|
||||
function onLocalVideo() {
|
||||
const el = selected.value
|
||||
if (!el) return
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'video/*'
|
||||
input.onchange = async () => {
|
||||
const f = input.files?.[0]
|
||||
if (!f) return
|
||||
// 注意:大视频会整体读入内存(dataURL),本地工具首版接受
|
||||
const dataUrl = await readAsDataURL(f)
|
||||
store.updateElement(el.id, { content: dataUrl })
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
function onVideoUrl(e: Event) {
|
||||
const el = selected.value
|
||||
if (!el) return
|
||||
const v = (e.target as HTMLInputElement).value.trim()
|
||||
if (v) store.updateElement(el.id, { content: v })
|
||||
}
|
||||
function toggleVideoOpt(key: 'autoplay' | 'loop' | 'muted') {
|
||||
const el = selected.value
|
||||
if (!el) return
|
||||
const cur = (el.style as any)[key]
|
||||
store.updateElement(el.id, { style: { [key]: !cur } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -247,6 +330,24 @@ function onLocalImage() {
|
||||
<button class="danger" @click="onMultiDel" title="Delete">🗑 删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label>对齐</label>
|
||||
<div class="seg">
|
||||
<button @click="onAlignOp('left')" title="左对齐">⇤</button>
|
||||
<button @click="onAlignOp('hcenter')" title="水平居中">↔</button>
|
||||
<button @click="onAlignOp('right')" title="右对齐">⇥</button>
|
||||
<button @click="onAlignOp('top')" title="顶对齐">⇧</button>
|
||||
<button @click="onAlignOp('vmiddle')" title="垂直居中">↕</button>
|
||||
<button @click="onAlignOp('bottom')" title="底对齐">⇩</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label>分布</label>
|
||||
<div class="seg">
|
||||
<button :disabled="multiCount < 3" @click="onDistribute('h')" :title="multiCount < 3 ? '需要选中 3 个以上' : '水平等间距'">⋯</button>
|
||||
<button :disabled="multiCount < 3" @click="onDistribute('v')" :title="multiCount < 3 ? '需要选中 3 个以上' : '垂直等间距'">⋮</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label>提示</label>
|
||||
<div style="font-size:12px;color:var(--ui-muted);line-height:1.6">
|
||||
@@ -262,7 +363,12 @@ function onLocalImage() {
|
||||
<!-- 内容 -->
|
||||
<div v-if="hasText && selected.type !== 'stat'" class="prop-row">
|
||||
<label>内容</label>
|
||||
<textarea rows="3" placeholder="输入文字(列表用换行分隔)" v-model="content" @input="onContentInput"></textarea>
|
||||
<textarea
|
||||
rows="3"
|
||||
:placeholder="selected.type === 'card' ? '第一行=标题,其余行=正文' : selected.type === 'shape' ? '形状内文字(支持 **加粗** 等 md 语法)' : '输入文字(列表用换行分隔)'"
|
||||
v-model="content"
|
||||
@input="onContentInput"
|
||||
></textarea>
|
||||
<!-- 富文本格式提示 -->
|
||||
<div v-if="['title','text','quote','list'].includes(selected.type)" class="rich-hint">
|
||||
<div class="rich-syntax">
|
||||
@@ -322,7 +428,15 @@ function onLocalImage() {
|
||||
<select v-model="shape" @change="onShapeChange">
|
||||
<option value="rect">矩形</option>
|
||||
<option value="circle">圆形</option>
|
||||
<option value="ellipse">椭圆</option>
|
||||
<option value="triangle">三角</option>
|
||||
<option value="diamond">菱形</option>
|
||||
<option value="pentagon">五边形</option>
|
||||
<option value="hexagon">六边形</option>
|
||||
<option value="star">五角星</option>
|
||||
<option value="arrow">箭头</option>
|
||||
<option value="chevron">折角</option>
|
||||
<option value="bubble">气泡</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -384,6 +498,31 @@ function onLocalImage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频来源(仅视频元素) -->
|
||||
<div v-if="selected.type === 'video'" class="prop-row">
|
||||
<label>视频来源</label>
|
||||
<div class="seg">
|
||||
<button @click="onLocalVideo" title="从本地选择视频文件">📁 本地视频</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
class="video-url"
|
||||
placeholder="或粘贴视频 URL (https://…)"
|
||||
style="margin-top:6px;width:100%;box-sizing:border-box"
|
||||
:value="selected.content"
|
||||
@change="onVideoUrl($event)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 视频播放选项 -->
|
||||
<div v-if="selected.type === 'video'" class="prop-row">
|
||||
<label>播放选项</label>
|
||||
<div class="seg">
|
||||
<button :class="{ active: !!selected.style.autoplay }" @click="toggleVideoOpt('autoplay')" title="自动播放">自动</button>
|
||||
<button :class="{ active: !!selected.style.loop }" @click="toggleVideoOpt('loop')" title="循环">循环</button>
|
||||
<button :class="{ active: !!selected.style.muted }" @click="toggleVideoOpt('muted')" title="静音">静音</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 层级 -->
|
||||
<div class="prop-row">
|
||||
<label>层级</label>
|
||||
@@ -393,6 +532,108 @@ function onLocalImage() {
|
||||
<button class="danger" @click="onDel" title="删除">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批注(仅图片元素) -->
|
||||
<div v-if="selected.type === 'image'" class="prop-row">
|
||||
<label>批注</label>
|
||||
<div class="anno-head">
|
||||
<button @click="addAnno" title="添加一条批注">+ 添加批注</button>
|
||||
</div>
|
||||
<div class="anno-list">
|
||||
<div v-for="(anno, i) in annotations" :key="anno.id" class="anno-card" :class="{ open: expandedAnno === anno.id }">
|
||||
<div class="anno-card-head" @click="toggleAnno(anno.id)">
|
||||
<span class="anno-caret">{{ expandedAnno === anno.id ? '▾' : '▸' }}</span>
|
||||
<span class="anno-card-title">批注 {{ i + 1 }}</span>
|
||||
<span v-if="expandedAnno !== anno.id" class="anno-card-summary">{{ annoSummary(anno) }}</span>
|
||||
<button class="anno-del" @click.stop="delAnno(anno.id)" title="删除批注">×</button>
|
||||
</div>
|
||||
|
||||
<div v-show="expandedAnno === anno.id" class="anno-body">
|
||||
<textarea
|
||||
class="anno-text"
|
||||
rows="2"
|
||||
placeholder="批注文字"
|
||||
:value="anno.text"
|
||||
@input="patchAnno(anno.id, { text: ($event.target as HTMLTextAreaElement).value })"
|
||||
></textarea>
|
||||
|
||||
<!-- 气泡文字样式 -->
|
||||
<div class="anno-sub">气泡文字</div>
|
||||
<div class="anno-line">
|
||||
<label class="anno-lbl">字号 {{ anno.fontSize || 16 }}px</label>
|
||||
<input
|
||||
type="range" min="12" max="48"
|
||||
:value="anno.fontSize || 16"
|
||||
@input="patchAnno(anno.id, { fontSize: Number(($event.target as HTMLInputElement).value) })"
|
||||
/>
|
||||
</div>
|
||||
<div class="anno-line">
|
||||
<select
|
||||
:value="anno.color || 'text'"
|
||||
@change="patchAnno(anno.id, { color: ($event.target as HTMLSelectElement).value })"
|
||||
>
|
||||
<option v-for="c in ANNO_COLORS" :key="c.k" :value="c.k">{{ c.label }}</option>
|
||||
</select>
|
||||
<div class="seg">
|
||||
<button :class="{ active: !!anno.bold }" @click="patchAnno(anno.id, { bold: !anno.bold })" title="加粗"><b>B</b></button>
|
||||
<button :class="{ active: !!anno.italic }" @click="patchAnno(anno.id, { italic: !anno.italic })" title="斜体"><i>I</i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="anno-line">
|
||||
<div class="seg">
|
||||
<button :class="{ active: anno.align === 'left' }" @click="patchAnno(anno.id, { align: 'left' })">⬅</button>
|
||||
<button :class="{ active: anno.align === 'center' || !anno.align }" @click="patchAnno(anno.id, { align: 'center' })">⬌</button>
|
||||
<button :class="{ active: anno.align === 'right' }" @click="patchAnno(anno.id, { align: 'right' })">➡</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连线样式 -->
|
||||
<div class="anno-sub">连线</div>
|
||||
<div class="anno-line">
|
||||
<select
|
||||
:value="anno.line?.style || 'solid'"
|
||||
@change="patchAnno(anno.id, { line: { style: ($event.target as HTMLSelectElement).value } })"
|
||||
>
|
||||
<option value="solid">实线</option>
|
||||
<option value="dashed">虚线</option>
|
||||
</select>
|
||||
<select
|
||||
:value="anno.line?.color || 'muted'"
|
||||
@change="patchAnno(anno.id, { line: { color: ($event.target as HTMLSelectElement).value } })"
|
||||
>
|
||||
<option v-for="c in ANNO_COLORS" :key="c.k" :value="c.k">{{ c.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="anno-line">
|
||||
<label class="anno-lbl">线宽 {{ anno.line?.width ?? 0.5 }}px</label>
|
||||
<input
|
||||
type="range" min="0.5" max="8" step="0.5"
|
||||
:value="anno.line?.width ?? 0.5"
|
||||
@input="patchAnno(anno.id, { line: { width: Number(($event.target as HTMLInputElement).value) } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="anno-line">
|
||||
<select
|
||||
:value="anno.line?.startCap || 'none'"
|
||||
@change="patchAnno(anno.id, { line: { startCap: ($event.target as HTMLSelectElement).value } })"
|
||||
>
|
||||
<option value="none">起点·无</option>
|
||||
<option value="arrow">起点·箭头</option>
|
||||
<option value="dot">起点·圆点</option>
|
||||
</select>
|
||||
<select
|
||||
:value="anno.line?.endCap || 'arrow'"
|
||||
@change="patchAnno(anno.id, { line: { endCap: ($event.target as HTMLSelectElement).value } })"
|
||||
>
|
||||
<option value="none">终点·无</option>
|
||||
<option value="arrow">终点·箭头</option>
|
||||
<option value="dot">终点·圆点</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 当前页背景 -->
|
||||
@@ -409,3 +650,126 @@ function onLocalImage() {
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.anno-head {
|
||||
display: flex;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.anno-head button {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
border: 1px solid var(--ui-border, #e2e8f0);
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
background: #fff;
|
||||
color: var(--ui-text, #1e293b);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background .15s, border-color .15s;
|
||||
}
|
||||
.anno-head button:hover {
|
||||
background: var(--ui-primary-soft, #eef2ff);
|
||||
border-color: var(--ui-primary, #4f46e5);
|
||||
color: var(--ui-primary, #4f46e5);
|
||||
}
|
||||
.anno-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.anno-card {
|
||||
border: 0.5px solid var(--ui-border, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--ui-panel, #f8fafc);
|
||||
}
|
||||
.anno-card.open {
|
||||
border-color: var(--ui-primary, #4f46e5);
|
||||
}
|
||||
.anno-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.anno-card-head:hover {
|
||||
background: var(--ui-primary-soft, #eef2ff);
|
||||
}
|
||||
.anno-caret {
|
||||
font-size: 10px;
|
||||
color: var(--ui-muted, #64748b);
|
||||
width: 12px;
|
||||
flex: none;
|
||||
}
|
||||
.anno-card-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--ui-text, #1e293b);
|
||||
flex: none;
|
||||
}
|
||||
.anno-card-summary {
|
||||
font-size: 12px;
|
||||
color: var(--ui-muted, #64748b);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.anno-del {
|
||||
margin-left: auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--ui-border, #e2e8f0);
|
||||
background: #fff;
|
||||
color: var(--ui-muted, #64748b);
|
||||
cursor: pointer;
|
||||
flex: none;
|
||||
}
|
||||
.anno-del:hover {
|
||||
background: #fff1f2;
|
||||
border-color: var(--ui-danger, #ef4444);
|
||||
color: var(--ui-danger, #ef4444);
|
||||
}
|
||||
.anno-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
.anno-text {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
.anno-sub {
|
||||
font-size: 11px;
|
||||
color: var(--ui-muted, #64748b);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.anno-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.anno-line > select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.anno-lbl {
|
||||
font-size: 12px;
|
||||
color: var(--ui-muted, #64748b);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.anno-line > input[type="range"] {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user