@h-ai/ui 0.1.0-alpha.11 → 0.1.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/compounds/Form.svelte +2 -0
- package/dist/components/compounds/Modal.svelte +251 -59
- package/dist/components/primitives/Button.svelte +2 -0
- package/dist/components/primitives/Switch.svelte +5 -0
- package/dist/components/scenes/ai/AiDocumentEditor.svelte +1474 -105
- package/dist/components/scenes/ai/MarkdownRenderer.svelte +2 -2
- package/dist/components/scenes/ai/document-download.js +249 -75
- package/dist/components/scenes/ai/document-parse.d.ts +8 -0
- package/dist/components/scenes/ai/document-parse.js +21 -6
- package/dist/components/scenes/ai/document-types.d.ts +30 -2
- package/dist/components/scenes/ai/editor-markdown-extensions.js +19 -4
- package/dist/components/scenes/ai/index.d.ts +2 -0
- package/dist/components/scenes/ai/index.js +1 -0
- package/dist/components/scenes/ai/table/AiTableEditor.svelte +1640 -0
- package/dist/components/scenes/ai/table/AiTableEditor.svelte.d.ts +6 -0
- package/dist/components/scenes/ai/table/table-types.d.ts +101 -0
- package/dist/components/scenes/ai/table/table-types.js +1 -0
- package/dist/messages/en-US.json +13 -0
- package/dist/messages/zh-CN.json +13 -0
- package/dist/types.d.ts +10 -0
- package/package.json +4 -2
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
MarkdownColorFormatRequest,
|
|
22
22
|
MarkdownInlineFormatKind,
|
|
23
23
|
MarkdownRewriteAction,
|
|
24
|
+
MarkdownSelectionSnapshot,
|
|
24
25
|
MarkdownTextAlignKind,
|
|
25
26
|
} from './document-types.js'
|
|
27
|
+
import { tick } from 'svelte'
|
|
26
28
|
import { uiM } from '../../../messages.js'
|
|
27
29
|
import { cn } from '../../../utils.js'
|
|
28
30
|
import AiDocumentDownloadMenu from './AiDocumentDownloadMenu.svelte'
|
|
@@ -33,7 +35,7 @@
|
|
|
33
35
|
interface SelectionToolbarPosition {
|
|
34
36
|
/** 选区工具条相对滚动容器的 top 坐标。 */
|
|
35
37
|
top: number
|
|
36
|
-
/**
|
|
38
|
+
/** 选区工具条左侧边缘相对滚动容器的 left 坐标。 */
|
|
37
39
|
left: number
|
|
38
40
|
/** 选区工具条应该出现在选区上方还是下方,避免遮挡正文。 */
|
|
39
41
|
placement: 'top' | 'bottom'
|
|
@@ -84,6 +86,28 @@
|
|
|
84
86
|
text: string
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
interface ColorSwatchOption {
|
|
90
|
+
/** 颜色值;为空表示恢复默认或清除当前维度。 */
|
|
91
|
+
value: string | null
|
|
92
|
+
/** 展示标题,用于 hover / a11y。 */
|
|
93
|
+
titleKey: 'markdown_background_none'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface HoveredLinkState {
|
|
97
|
+
/** 当前悬停的链接元素。 */
|
|
98
|
+
element: HTMLAnchorElement
|
|
99
|
+
/** 浏览器可直接打开的绝对链接地址。 */
|
|
100
|
+
href: string
|
|
101
|
+
/** 编辑时优先回填原始 href 属性,避免相对地址被强制绝对化。 */
|
|
102
|
+
rawHref: string
|
|
103
|
+
/** 悬停浮层相对滚动容器的 top 坐标。 */
|
|
104
|
+
top: number
|
|
105
|
+
/** 悬停浮层左侧边缘相对滚动容器的 left 坐标。 */
|
|
106
|
+
left: number
|
|
107
|
+
/** 浮层出现在链接上方还是下方。 */
|
|
108
|
+
placement: 'top' | 'bottom'
|
|
109
|
+
}
|
|
110
|
+
|
|
87
111
|
type SelectionMenuKind = 'rewrite' | 'block' | 'align' | 'link' | 'color'
|
|
88
112
|
|
|
89
113
|
const BLOCK_FORMAT_OPTIONS: Array<{
|
|
@@ -94,6 +118,8 @@
|
|
|
94
118
|
| 'markdown_format_heading_2'
|
|
95
119
|
| 'markdown_format_heading_3'
|
|
96
120
|
| 'markdown_format_heading_4'
|
|
121
|
+
| 'markdown_format_ordered_list'
|
|
122
|
+
| 'markdown_format_bullet_list'
|
|
97
123
|
shortLabel: string
|
|
98
124
|
}> = [
|
|
99
125
|
{
|
|
@@ -116,6 +142,16 @@
|
|
|
116
142
|
labelKey: 'markdown_format_heading_3',
|
|
117
143
|
shortLabel: 'H3',
|
|
118
144
|
},
|
|
145
|
+
{
|
|
146
|
+
value: 'orderedList',
|
|
147
|
+
labelKey: 'markdown_format_ordered_list',
|
|
148
|
+
shortLabel: '1.',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
value: 'bulletList',
|
|
152
|
+
labelKey: 'markdown_format_bullet_list',
|
|
153
|
+
shortLabel: '•',
|
|
154
|
+
},
|
|
119
155
|
{
|
|
120
156
|
value: 'heading4',
|
|
121
157
|
labelKey: 'markdown_format_heading_4',
|
|
@@ -158,6 +194,13 @@
|
|
|
158
194
|
'#f3f4f6',
|
|
159
195
|
'#e2e8f0',
|
|
160
196
|
] as const
|
|
197
|
+
const BACKGROUND_COLOR_OPTIONS: readonly (typeof BACKGROUND_COLOR_PRESETS[number] | ColorSwatchOption)[] = [
|
|
198
|
+
{ value: null, titleKey: 'markdown_background_none' },
|
|
199
|
+
...BACKGROUND_COLOR_PRESETS,
|
|
200
|
+
] as const
|
|
201
|
+
const FALLBACK_BLOCK_FORMAT_OPTION = BLOCK_FORMAT_OPTIONS.find(
|
|
202
|
+
option => option.value === 'paragraph',
|
|
203
|
+
) ?? BLOCK_FORMAT_OPTIONS[0]
|
|
161
204
|
|
|
162
205
|
const DEFAULT_SELECTION_FORMAT_STATE: SelectionFormatState = {
|
|
163
206
|
blockFormat: 'paragraph',
|
|
@@ -176,7 +219,6 @@
|
|
|
176
219
|
// 复制前后的图标以内联 SVG 缓存,避免每次点击都重新拼接按钮内容。
|
|
177
220
|
const COPY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`
|
|
178
221
|
const CHECK_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`
|
|
179
|
-
const HTML_SNIPPET_REGEX = /<!doctype html>|<html[\s>]|<body[\s>]|<div[\s>]|<main[\s>]/i
|
|
180
222
|
|
|
181
223
|
let {
|
|
182
224
|
// 暴露给外层的滚动容器引用,用于同步滚动或定位选区工具条。
|
|
@@ -197,6 +239,10 @@
|
|
|
197
239
|
showCopyButton = true,
|
|
198
240
|
// 是否显示代码块运行按钮与预览占位。
|
|
199
241
|
showRunButton = false,
|
|
242
|
+
// 是否在代码块头部显示“代码/预览”切换。
|
|
243
|
+
showCodePreviewToggle = false,
|
|
244
|
+
// 代码预览切换模式下展示在语言标签旁的提示文案。
|
|
245
|
+
codePreviewHint = '',
|
|
200
246
|
// 是否启用语法高亮。
|
|
201
247
|
enableHighlight = true,
|
|
202
248
|
// 是否把换行渲染为 <br>。
|
|
@@ -286,6 +332,10 @@
|
|
|
286
332
|
placement: 'top',
|
|
287
333
|
alignment: 'center',
|
|
288
334
|
})
|
|
335
|
+
// 窄容器下目录会改成悬浮覆盖模式,保存 DOM 引用后才能在定位工具条时主动避让。
|
|
336
|
+
let outlinePanelEl = $state<HTMLElement | null>(null)
|
|
337
|
+
// chrome 节点同时包住主工具条和下拉面板,方便判断焦点是否仍留在浮层内部。
|
|
338
|
+
let selectionChromeEl = $state<HTMLDivElement | null>(null)
|
|
289
339
|
// 选区工具条自身 DOM,用于根据真实尺寸修正贴边位置。
|
|
290
340
|
let selectionToolbarEl = $state<HTMLDivElement | null>(null)
|
|
291
341
|
// 当前展开菜单对应的触发按钮,用来把面板锚定到具体按钮下方/上方。
|
|
@@ -306,24 +356,48 @@
|
|
|
306
356
|
})
|
|
307
357
|
// linkDraft 只在链接弹层里暂存输入值,避免每次打开都丢掉正在编辑的链接。
|
|
308
358
|
let linkDraft = $state('')
|
|
359
|
+
// 链接输入框允许接管焦点,但格式动作仍需要恢复到正文的最后一个有效选区。
|
|
360
|
+
let linkInputEl = $state<HTMLInputElement | null>(null)
|
|
361
|
+
// 链接按钮 DOM 用来在“悬停编辑链接”场景下复用现有链接弹层定位。
|
|
362
|
+
let selectionLinkButtonEl = $state<HTMLButtonElement | null>(null)
|
|
363
|
+
// 原始 DOM Range 在结构没变时恢复最快,也能保留浏览器原生的精确边界。
|
|
364
|
+
let savedSelectionRange = $state<Range | null>(null)
|
|
365
|
+
// 纯文本快照用于 DOM 被局部重建后的兜底恢复,避免只靠 Range 引用失效。
|
|
366
|
+
let savedSelectionSnapshot = $state<MarkdownSelectionSnapshot | null>(null)
|
|
309
367
|
// 真实尺寸修正只需要排队一帧,避免反复触发 handleSelectionChange 形成抖动。
|
|
310
368
|
let selectionToolbarMeasurePending = false
|
|
311
369
|
// 选区刷新统一合并到同一帧,避免 selectionchange / mouseup / blur 连续触发时抖动。
|
|
312
370
|
let selectionRefreshFrame: number | undefined
|
|
371
|
+
// 同一帧内任一来源要求重新量尺时,都要把这次刷新升级成 remeasure 版本。
|
|
313
372
|
let selectionRefreshNeedsRemeasure = false
|
|
314
373
|
// 浏览器在滚动到底部或切换焦点时会短暂给出空选区,这里延迟一拍再关闭工具条。
|
|
315
374
|
let selectionToolbarCloseTimer: number | undefined
|
|
375
|
+
// 悬停到已配置链接时显示快捷浮层,便于直接打开或编辑。
|
|
376
|
+
let hoveredLinkState = $state<HoveredLinkState | null>(null)
|
|
377
|
+
// 悬停浮层需要拿到真实尺寸后再二次校正位置。
|
|
378
|
+
let hoveredLinkPopoverEl = $state<HTMLDivElement | null>(null)
|
|
379
|
+
// 鼠标从正文移动到悬停浮层时给一小段缓冲,避免浮层闪烁。
|
|
380
|
+
let hoveredLinkHideTimer: number | undefined
|
|
316
381
|
|
|
317
382
|
// code 类型产物通常只有裸代码文本,这里统一包成 fenced block 进入同一条渲染链路。
|
|
318
383
|
const documentContent = $derived(
|
|
319
384
|
resolveDocumentMarkdownContent(content, sourceKind, codeLanguage),
|
|
320
385
|
)
|
|
321
386
|
// 渲染结果同时提供 HTML、目录和代码块元数据,供顶部目录与代码预览共用。
|
|
387
|
+
const codePreviewToggleEnabled = $derived(
|
|
388
|
+
showCodePreviewToggle && sourceKind === 'code',
|
|
389
|
+
)
|
|
390
|
+
// hint 文案统一做 trim,避免只传空格时仍把占位区域撑开。
|
|
391
|
+
const normalizedCodePreviewHint = $derived(codePreviewHint.trim())
|
|
322
392
|
const renderResult = $derived(
|
|
323
393
|
renderMarkdownDocument(documentContent, {
|
|
324
394
|
enableHighlight,
|
|
325
395
|
showCopyButton,
|
|
326
396
|
showRunButton,
|
|
397
|
+
showCodePreviewToggle: codePreviewToggleEnabled,
|
|
398
|
+
codeViewCodeLabel: uiM('markdown_format_code'),
|
|
399
|
+
codeViewPreviewLabel: uiM('file_list_preview'),
|
|
400
|
+
codePreviewHint: normalizedCodePreviewHint,
|
|
327
401
|
breaks,
|
|
328
402
|
}),
|
|
329
403
|
)
|
|
@@ -337,6 +411,10 @@
|
|
|
337
411
|
const resolvedRewriteActions = $derived(
|
|
338
412
|
resolveRewriteActions(rewriteActions, onrewrite),
|
|
339
413
|
)
|
|
414
|
+
// 只有一个改写动作时直接渲染成按钮,多动作才保留菜单,减少一次点击。
|
|
415
|
+
const directRewriteAction = $derived(
|
|
416
|
+
resolvedRewriteActions.length === 1 ? resolvedRewriteActions[0] : null,
|
|
417
|
+
)
|
|
340
418
|
// 新版工具条支持 paragraph / heading1-4,优先走细粒度回调,避免把旧接口误当成新接口调用。
|
|
341
419
|
const richBlockFormattingEnabled = $derived(Boolean(onapplyblockstyle))
|
|
342
420
|
// 旧版 heading / bullet 回调仍然保留,用于兼容外部还没迁移的调用方。
|
|
@@ -364,13 +442,14 @@
|
|
|
364
442
|
const activeBlockFormatOption = $derived(
|
|
365
443
|
BLOCK_FORMAT_OPTIONS.find(
|
|
366
444
|
option => option.value === selectionFormatState.blockFormat,
|
|
367
|
-
) ??
|
|
445
|
+
) ?? FALLBACK_BLOCK_FORMAT_OPTION,
|
|
368
446
|
)
|
|
369
447
|
// readerDocumentClass 只负责正文文章区域,不和外层容器类名混用。
|
|
370
448
|
const readerDocumentClass = $derived(
|
|
371
449
|
cn(
|
|
372
450
|
'hai-markdown',
|
|
373
451
|
'hai-markdown-document',
|
|
452
|
+
sourceKind === 'code' ? 'hai-markdown-code-only' : '',
|
|
374
453
|
editable ? 'hai-markdown-editable' : '',
|
|
375
454
|
),
|
|
376
455
|
)
|
|
@@ -416,7 +495,9 @@
|
|
|
416
495
|
|
|
417
496
|
$effect(() => {
|
|
418
497
|
void documentContent
|
|
419
|
-
|
|
498
|
+
codePreviews = {}
|
|
499
|
+
hoveredLinkState = null
|
|
500
|
+
queueSelectionRefresh(true)
|
|
420
501
|
})
|
|
421
502
|
|
|
422
503
|
$effect(() => {
|
|
@@ -441,6 +522,10 @@
|
|
|
441
522
|
if (selectionToolbarCloseTimer) {
|
|
442
523
|
window.clearTimeout(selectionToolbarCloseTimer)
|
|
443
524
|
}
|
|
525
|
+
|
|
526
|
+
if (hoveredLinkHideTimer) {
|
|
527
|
+
window.clearTimeout(hoveredLinkHideTimer)
|
|
528
|
+
}
|
|
444
529
|
}
|
|
445
530
|
})
|
|
446
531
|
|
|
@@ -463,6 +548,64 @@
|
|
|
463
548
|
}
|
|
464
549
|
})
|
|
465
550
|
|
|
551
|
+
$effect(() => {
|
|
552
|
+
if (
|
|
553
|
+
typeof document === 'undefined'
|
|
554
|
+
|| !selectionToolbarVisible
|
|
555
|
+
) {
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const handleDocumentPointerDown = (event: MouseEvent): void => {
|
|
560
|
+
const target = event.target
|
|
561
|
+
if (!(target instanceof Node)) {
|
|
562
|
+
return
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (selectionChromeEl?.contains(target)) {
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
closeSelectionToolbar()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
document.addEventListener('mousedown', handleDocumentPointerDown, true)
|
|
573
|
+
|
|
574
|
+
return () => {
|
|
575
|
+
document.removeEventListener('mousedown', handleDocumentPointerDown, true)
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
$effect(() => {
|
|
580
|
+
if (typeof window === 'undefined' || activeSelectionMenu !== 'link' || !linkInputEl) {
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const nextLinkInput = linkInputEl
|
|
585
|
+
const focusFrame = window.requestAnimationFrame(() => {
|
|
586
|
+
nextLinkInput.focus()
|
|
587
|
+
nextLinkInput.select()
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
return () => {
|
|
591
|
+
window.cancelAnimationFrame(focusFrame)
|
|
592
|
+
}
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
$effect(() => {
|
|
596
|
+
if (typeof window === 'undefined' || !hoveredLinkState || !hoveredLinkPopoverEl) {
|
|
597
|
+
return
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const measureFrame = window.requestAnimationFrame(() => {
|
|
601
|
+
syncHoveredLinkPosition()
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
return () => {
|
|
605
|
+
window.cancelAnimationFrame(measureFrame)
|
|
606
|
+
}
|
|
607
|
+
})
|
|
608
|
+
|
|
466
609
|
function resolveRewriteActions(
|
|
467
610
|
actions: MarkdownRewriteAction[],
|
|
468
611
|
handler?: AiDocumentEditorProps['onrewrite'],
|
|
@@ -483,6 +626,8 @@
|
|
|
483
626
|
]
|
|
484
627
|
}
|
|
485
628
|
|
|
629
|
+
const HTML_DOCUMENT_PATTERN = /<!doctype html>|<html[\s>]|<body[\s>]|<div[\s>]|<main[\s>]/i
|
|
630
|
+
|
|
486
631
|
async function copyRawContent(): Promise<boolean> {
|
|
487
632
|
try {
|
|
488
633
|
await navigator.clipboard.writeText(content)
|
|
@@ -549,9 +694,7 @@
|
|
|
549
694
|
}
|
|
550
695
|
|
|
551
696
|
function looksLikeHtml(code: string): boolean {
|
|
552
|
-
return
|
|
553
|
-
code,
|
|
554
|
-
)
|
|
697
|
+
return HTML_DOCUMENT_PATTERN.test(code)
|
|
555
698
|
}
|
|
556
699
|
|
|
557
700
|
function buildJavaScriptPreview(code: string): string {
|
|
@@ -668,6 +811,38 @@ ${safeCode}
|
|
|
668
811
|
return undefined
|
|
669
812
|
}
|
|
670
813
|
|
|
814
|
+
function formatPreviewLanguageLabel(language: string | undefined): string | null {
|
|
815
|
+
const normalized = language?.trim().toLocaleLowerCase()
|
|
816
|
+
if (!normalized) {
|
|
817
|
+
return null
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (
|
|
821
|
+
normalized === 'javascript'
|
|
822
|
+
|| normalized === 'js'
|
|
823
|
+
|| normalized === 'mjs'
|
|
824
|
+
) {
|
|
825
|
+
return 'JavaScript'
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (normalized === 'markdown' || normalized === 'md') {
|
|
829
|
+
return 'Markdown'
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return normalized.toUpperCase()
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function resolvePreviewUnavailableMessage(language: string | undefined): string {
|
|
836
|
+
const languageLabel = formatPreviewLanguageLabel(language)
|
|
837
|
+
if (!languageLabel) {
|
|
838
|
+
return uiM('markdown_run_unavailable')
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return uiM('markdown_run_unavailable_language', {
|
|
842
|
+
language: languageLabel,
|
|
843
|
+
})
|
|
844
|
+
}
|
|
845
|
+
|
|
671
846
|
async function runCodeBlock(blockId: string): Promise<void> {
|
|
672
847
|
const codeBlock = lookupCodeBlock(blockId)
|
|
673
848
|
if (!codeBlock) {
|
|
@@ -698,7 +873,7 @@ ${safeCode}
|
|
|
698
873
|
...codePreviews,
|
|
699
874
|
[blockId]: {
|
|
700
875
|
status: 'error',
|
|
701
|
-
error:
|
|
876
|
+
error: resolvePreviewUnavailableMessage(request.language),
|
|
702
877
|
},
|
|
703
878
|
}
|
|
704
879
|
return
|
|
@@ -724,6 +899,24 @@ ${safeCode}
|
|
|
724
899
|
}
|
|
725
900
|
}
|
|
726
901
|
|
|
902
|
+
function setCodeBlockView(
|
|
903
|
+
codeBlock: HTMLElement,
|
|
904
|
+
view: 'code' | 'preview',
|
|
905
|
+
): void {
|
|
906
|
+
codeBlock.dataset.codeView = view
|
|
907
|
+
const toggleButtons = codeBlock.querySelectorAll<HTMLButtonElement>(
|
|
908
|
+
'[data-code-view-toggle]',
|
|
909
|
+
)
|
|
910
|
+
for (const button of toggleButtons) {
|
|
911
|
+
const isActive = button.dataset.codeView === view
|
|
912
|
+
button.setAttribute('aria-pressed', isActive ? 'true' : 'false')
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* 统一承接正文里通过 `{@html}` 注入的交互节点点击。
|
|
918
|
+
* 这些按钮和链接不受 Svelte 逐个绑定管理,因此在容器级别做代理更稳妥。
|
|
919
|
+
*/
|
|
727
920
|
async function handleClick(event: MouseEvent): Promise<void> {
|
|
728
921
|
const target = event.target as HTMLElement
|
|
729
922
|
const copyButton = target.closest(
|
|
@@ -734,12 +927,48 @@ ${safeCode}
|
|
|
734
927
|
return
|
|
735
928
|
}
|
|
736
929
|
|
|
930
|
+
const viewButton = target.closest(
|
|
931
|
+
'[data-code-view-toggle]',
|
|
932
|
+
) as HTMLButtonElement | null
|
|
933
|
+
const viewCodeBlock = viewButton?.closest<HTMLElement>('.hai-md-code-block')
|
|
934
|
+
const viewCodeBlockId = viewButton?.dataset.codeBlockId
|
|
935
|
+
const nextView = viewButton?.dataset.codeView === 'preview'
|
|
936
|
+
? 'preview'
|
|
937
|
+
: 'code'
|
|
938
|
+
if (viewButton && viewCodeBlock) {
|
|
939
|
+
setCodeBlockView(viewCodeBlock, nextView)
|
|
940
|
+
if (
|
|
941
|
+
nextView === 'preview'
|
|
942
|
+
&& viewCodeBlockId
|
|
943
|
+
&& codePreviews[viewCodeBlockId]?.status !== 'ready'
|
|
944
|
+
) {
|
|
945
|
+
await runCodeBlock(viewCodeBlockId)
|
|
946
|
+
}
|
|
947
|
+
return
|
|
948
|
+
}
|
|
949
|
+
|
|
737
950
|
const runButton = target.closest(
|
|
738
951
|
'[data-run-code]',
|
|
739
952
|
) as HTMLButtonElement | null
|
|
740
953
|
const codeBlockId = runButton?.dataset.codeBlockId
|
|
741
954
|
if (runButton && codeBlockId) {
|
|
742
955
|
await runCodeBlock(codeBlockId)
|
|
956
|
+
return
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const link = target.closest('a[href]') as HTMLAnchorElement | null
|
|
960
|
+
if (editable && link && previewHost?.contains(link)) {
|
|
961
|
+
const href = link.href.trim()
|
|
962
|
+
if (!href) {
|
|
963
|
+
return
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
event.preventDefault()
|
|
967
|
+
window.open(
|
|
968
|
+
href,
|
|
969
|
+
link.target || '_blank',
|
|
970
|
+
'noopener,noreferrer',
|
|
971
|
+
)
|
|
743
972
|
}
|
|
744
973
|
}
|
|
745
974
|
|
|
@@ -775,12 +1004,12 @@ ${safeCode}
|
|
|
775
1004
|
}
|
|
776
1005
|
|
|
777
1006
|
if (preview.status === 'running') {
|
|
778
|
-
host.innerHTML = `<div class="hai-md-preview-card"><div class="hai-md-preview-
|
|
1007
|
+
host.innerHTML = `<div class="hai-md-preview-card"><div class="hai-md-preview-empty"><div class="hai-md-preview-empty-title">${escapePreviewText(uiM('markdown_running'))}</div><div class="hai-md-preview-loading"></div></div></div>`
|
|
779
1008
|
return
|
|
780
1009
|
}
|
|
781
1010
|
|
|
782
1011
|
if (preview.status === 'error') {
|
|
783
|
-
host.innerHTML = `<div class="hai-md-preview-card hai-md-preview-error"><div class="hai-md-preview-
|
|
1012
|
+
host.innerHTML = `<div class="hai-md-preview-card hai-md-preview-error"><div class="hai-md-preview-empty"><div class="hai-md-preview-empty-title">${escapePreviewText(uiM('markdown_run_failed'))}</div><p class="hai-md-preview-empty-text">${escapePreviewText(preview.error ?? uiM('markdown_run_failed'))}</p></div></div>`
|
|
784
1013
|
return
|
|
785
1014
|
}
|
|
786
1015
|
|
|
@@ -881,7 +1110,18 @@ ${safeCode}
|
|
|
881
1110
|
}
|
|
882
1111
|
|
|
883
1112
|
function readBlockFormat(element: HTMLElement | null): MarkdownBlockStyleKind {
|
|
884
|
-
const
|
|
1113
|
+
const block = getClosestBlockElement(element)
|
|
1114
|
+
const tagName = block?.tagName.toLowerCase()
|
|
1115
|
+
|
|
1116
|
+
if (tagName === 'li') {
|
|
1117
|
+
const listTag = block?.parentElement?.tagName.toLowerCase()
|
|
1118
|
+
if (listTag === 'ol') {
|
|
1119
|
+
return 'orderedList'
|
|
1120
|
+
}
|
|
1121
|
+
if (listTag === 'ul') {
|
|
1122
|
+
return 'bulletList'
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
885
1125
|
|
|
886
1126
|
if (tagName === 'h1') {
|
|
887
1127
|
return 'heading1'
|
|
@@ -1008,6 +1248,370 @@ ${safeCode}
|
|
|
1008
1248
|
}
|
|
1009
1249
|
}
|
|
1010
1250
|
|
|
1251
|
+
/**
|
|
1252
|
+
* 下面这组 helper 在 contenteditable DOM 与“正文纯文本偏移”之间来回映射。
|
|
1253
|
+
* 选区工具条一旦失焦或内容局部重渲染,就靠它们把原选区重新定位回来。
|
|
1254
|
+
*/
|
|
1255
|
+
function getNodeTextLength(node: Node | null | undefined): number {
|
|
1256
|
+
if (!node) {
|
|
1257
|
+
return 0
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1261
|
+
return node.textContent?.length ?? 0
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
let length = 0
|
|
1265
|
+
for (const childNode of Array.from(node.childNodes)) {
|
|
1266
|
+
length += getNodeTextLength(childNode)
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
return length
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function getTextOffsetWithinNode(node: Node, offset: number): number {
|
|
1273
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1274
|
+
return Math.max(0, Math.min(offset, node.textContent?.length ?? 0))
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const childNodes = Array.from(node.childNodes)
|
|
1278
|
+
const nextOffset = Math.max(0, Math.min(offset, childNodes.length))
|
|
1279
|
+
let length = 0
|
|
1280
|
+
|
|
1281
|
+
for (const childNode of childNodes.slice(0, nextOffset)) {
|
|
1282
|
+
length += getNodeTextLength(childNode)
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
return length
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function getTextOffsetBeforeNode(root: Node, target: Node): number | null {
|
|
1289
|
+
let length = 0
|
|
1290
|
+
|
|
1291
|
+
const visit = (node: Node): boolean => {
|
|
1292
|
+
if (node === target) {
|
|
1293
|
+
return true
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1297
|
+
length += node.textContent?.length ?? 0
|
|
1298
|
+
return false
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
for (const childNode of Array.from(node.childNodes)) {
|
|
1302
|
+
if (visit(childNode)) {
|
|
1303
|
+
return true
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
return false
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
return visit(root) ? length : null
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function getTextOffsetInPreview(node: Node, offset: number): number | null {
|
|
1314
|
+
if (!previewHost || (node !== previewHost && !previewHost.contains(node))) {
|
|
1315
|
+
return null
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const prefixLength = getTextOffsetBeforeNode(previewHost, node)
|
|
1319
|
+
if (prefixLength === null) {
|
|
1320
|
+
return null
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
return prefixLength + getTextOffsetWithinNode(node, offset)
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function resolveTextPositionInPreview(
|
|
1327
|
+
offset: number,
|
|
1328
|
+
): { node: Node, offset: number } | null {
|
|
1329
|
+
if (!previewHost) {
|
|
1330
|
+
return null
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const targetOffset = Math.max(0, offset)
|
|
1334
|
+
const walker = document.createTreeWalker(previewHost, NodeFilter.SHOW_TEXT)
|
|
1335
|
+
let current = walker.nextNode()
|
|
1336
|
+
let accumulatedLength = 0
|
|
1337
|
+
let lastTextNode: Node | null = null
|
|
1338
|
+
|
|
1339
|
+
while (current) {
|
|
1340
|
+
const textLength = current.textContent?.length ?? 0
|
|
1341
|
+
if (targetOffset <= accumulatedLength + textLength) {
|
|
1342
|
+
return {
|
|
1343
|
+
node: current,
|
|
1344
|
+
offset: Math.max(0, targetOffset - accumulatedLength),
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
accumulatedLength += textLength
|
|
1349
|
+
lastTextNode = current
|
|
1350
|
+
current = walker.nextNode()
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (lastTextNode) {
|
|
1354
|
+
return {
|
|
1355
|
+
node: lastTextNode,
|
|
1356
|
+
offset: lastTextNode.textContent?.length ?? 0,
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
return {
|
|
1361
|
+
node: previewHost,
|
|
1362
|
+
offset: Math.min(targetOffset, previewHost.childNodes.length),
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* 把当前 Range 压缩成纯文本偏移快照。
|
|
1368
|
+
* 只记录非空选区,避免工具条动作把折叠光标也当成可恢复选区。
|
|
1369
|
+
*/
|
|
1370
|
+
function capturePreviewSelectionSnapshot(
|
|
1371
|
+
range: Range,
|
|
1372
|
+
): MarkdownSelectionSnapshot | null {
|
|
1373
|
+
if (!previewHost) {
|
|
1374
|
+
return null
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const start = getTextOffsetInPreview(range.startContainer, range.startOffset)
|
|
1378
|
+
const end = getTextOffsetInPreview(range.endContainer, range.endOffset)
|
|
1379
|
+
const text = range.toString()
|
|
1380
|
+
|
|
1381
|
+
if (start === null || end === null || end <= start || !text.trim()) {
|
|
1382
|
+
return null
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
return { start, end, text }
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* 用快照重新读取一遍当前正文中的文本,用来判断 DOM 变化后原 offsets 是否仍然可信。
|
|
1390
|
+
*/
|
|
1391
|
+
function readTextFromSnapshot(
|
|
1392
|
+
snapshot: MarkdownSelectionSnapshot,
|
|
1393
|
+
): string | null {
|
|
1394
|
+
const startPosition = resolveTextPositionInPreview(snapshot.start)
|
|
1395
|
+
const endPosition = resolveTextPositionInPreview(snapshot.end)
|
|
1396
|
+
if (!startPosition || !endPosition) {
|
|
1397
|
+
return null
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
try {
|
|
1401
|
+
const range = document.createRange()
|
|
1402
|
+
range.setStart(startPosition.node, startPosition.offset)
|
|
1403
|
+
range.setEnd(endPosition.node, endPosition.offset)
|
|
1404
|
+
return range.toString()
|
|
1405
|
+
}
|
|
1406
|
+
catch {
|
|
1407
|
+
return null
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* 当原 offsets 对应的文本已经变了时,按“离旧起点最近”的同文本片段回退。
|
|
1413
|
+
* 这样比只取第一个命中更接近用户刚才真实操作的位置。
|
|
1414
|
+
*/
|
|
1415
|
+
function findClosestSelectionOffsets(
|
|
1416
|
+
snapshot: MarkdownSelectionSnapshot,
|
|
1417
|
+
): { start: number, end: number } | null {
|
|
1418
|
+
if (!previewHost || !snapshot.text) {
|
|
1419
|
+
return null
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const textContent = previewHost.textContent ?? ''
|
|
1423
|
+
let bestMatchStart = -1
|
|
1424
|
+
let smallestDistance = Number.POSITIVE_INFINITY
|
|
1425
|
+
let searchIndex = textContent.indexOf(snapshot.text)
|
|
1426
|
+
|
|
1427
|
+
while (searchIndex >= 0) {
|
|
1428
|
+
const distance = Math.abs(searchIndex - snapshot.start)
|
|
1429
|
+
if (distance < smallestDistance) {
|
|
1430
|
+
smallestDistance = distance
|
|
1431
|
+
bestMatchStart = searchIndex
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
searchIndex = textContent.indexOf(snapshot.text, searchIndex + 1)
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
if (bestMatchStart < 0) {
|
|
1438
|
+
return null
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
return {
|
|
1442
|
+
start: bestMatchStart,
|
|
1443
|
+
end: bestMatchStart + snapshot.text.length,
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* 优先按纯文本偏移恢复选区;如果 DOM 结构变动导致 offsets 漂移,再退回最近文本片段。
|
|
1449
|
+
* 这一步会主动把焦点拉回正文,保证后续格式化命令仍作用于内容区。
|
|
1450
|
+
*/
|
|
1451
|
+
function restoreSelectionFromSnapshot(
|
|
1452
|
+
snapshot: MarkdownSelectionSnapshot,
|
|
1453
|
+
): boolean {
|
|
1454
|
+
if (typeof window === 'undefined' || !previewHost) {
|
|
1455
|
+
return false
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const selection = window.getSelection()
|
|
1459
|
+
if (!selection || snapshot.end <= snapshot.start) {
|
|
1460
|
+
return false
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
let nextOffsets = {
|
|
1464
|
+
start: snapshot.start,
|
|
1465
|
+
end: snapshot.end,
|
|
1466
|
+
}
|
|
1467
|
+
const currentText = readTextFromSnapshot(snapshot)
|
|
1468
|
+
if (snapshot.text && currentText !== snapshot.text) {
|
|
1469
|
+
const matchedOffsets = findClosestSelectionOffsets(snapshot)
|
|
1470
|
+
if (!matchedOffsets) {
|
|
1471
|
+
return false
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
nextOffsets = matchedOffsets
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const startPosition = resolveTextPositionInPreview(nextOffsets.start)
|
|
1478
|
+
const endPosition = resolveTextPositionInPreview(nextOffsets.end)
|
|
1479
|
+
if (!startPosition || !endPosition) {
|
|
1480
|
+
return false
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
try {
|
|
1484
|
+
const range = document.createRange()
|
|
1485
|
+
range.setStart(startPosition.node, startPosition.offset)
|
|
1486
|
+
range.setEnd(endPosition.node, endPosition.offset)
|
|
1487
|
+
previewHost.focus()
|
|
1488
|
+
selection.removeAllRanges()
|
|
1489
|
+
selection.addRange(range)
|
|
1490
|
+
return true
|
|
1491
|
+
}
|
|
1492
|
+
catch {
|
|
1493
|
+
return false
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/**
|
|
1498
|
+
* 纯文本匹配是最后一道兜底,只在 Range 和偏移快照都失效时使用。
|
|
1499
|
+
* 它无法区分完全相同的多个片段,所以只适合作为尽力恢复方案。
|
|
1500
|
+
*/
|
|
1501
|
+
function restoreSelectionByTextMatch(text: string): boolean {
|
|
1502
|
+
if (typeof window === 'undefined' || !previewHost || !text.trim()) {
|
|
1503
|
+
return false
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const selection = window.getSelection()
|
|
1507
|
+
if (!selection) {
|
|
1508
|
+
return false
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
const walker = document.createTreeWalker(previewHost, NodeFilter.SHOW_TEXT)
|
|
1512
|
+
let current = walker.nextNode()
|
|
1513
|
+
|
|
1514
|
+
while (current) {
|
|
1515
|
+
const content = current.textContent ?? ''
|
|
1516
|
+
const start = content.indexOf(text)
|
|
1517
|
+
if (start >= 0) {
|
|
1518
|
+
try {
|
|
1519
|
+
const range = document.createRange()
|
|
1520
|
+
range.setStart(current, start)
|
|
1521
|
+
range.setEnd(current, start + text.length)
|
|
1522
|
+
previewHost.focus()
|
|
1523
|
+
selection.removeAllRanges()
|
|
1524
|
+
selection.addRange(range)
|
|
1525
|
+
return true
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
return false
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
current = walker.nextNode()
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
return false
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* 工具条包含输入框和下拉面板,焦点落在内部时不应把“正文失焦”误判成关闭信号。
|
|
1540
|
+
*/
|
|
1541
|
+
function isSelectionChromeFocused(): boolean {
|
|
1542
|
+
if (typeof document === 'undefined' || !selectionChromeEl) {
|
|
1543
|
+
return false
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const activeElement = document.activeElement
|
|
1547
|
+
return activeElement instanceof HTMLElement && selectionChromeEl.contains(activeElement)
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* 同时保存浏览器原生 Range 和纯文本快照。
|
|
1552
|
+
* 前者恢复精度更高,后者能跨 DOM 重建,两者组合能覆盖大多数编辑场景。
|
|
1553
|
+
*/
|
|
1554
|
+
function rememberPreviewSelection(range: Range): void {
|
|
1555
|
+
savedSelectionRange = range.cloneRange()
|
|
1556
|
+
savedSelectionSnapshot = capturePreviewSelectionSnapshot(range)
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* 选区恢复按“快照 -> 原始 Range -> 文本兜底”三层顺序尝试。
|
|
1561
|
+
* 这样既优先命中最精确的位置,也能在内容轻微重排后保留工具条操作连续性。
|
|
1562
|
+
*/
|
|
1563
|
+
function restoreSavedPreviewSelection(): boolean {
|
|
1564
|
+
if (savedSelectionSnapshot && restoreSelectionFromSnapshot(savedSelectionSnapshot)) {
|
|
1565
|
+
return true
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (typeof window === 'undefined' || !previewHost || !savedSelectionRange) {
|
|
1569
|
+
const fallbackText = savedSelectionSnapshot?.text || selectedText
|
|
1570
|
+
return restoreSelectionByTextMatch(fallbackText)
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
const selection = window.getSelection()
|
|
1574
|
+
if (!selection) {
|
|
1575
|
+
return false
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
try {
|
|
1579
|
+
const restoredRange = savedSelectionRange.cloneRange()
|
|
1580
|
+
if (
|
|
1581
|
+
!previewHost.contains(restoredRange.startContainer)
|
|
1582
|
+
|| !previewHost.contains(restoredRange.endContainer)
|
|
1583
|
+
) {
|
|
1584
|
+
return false
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
previewHost.focus()
|
|
1588
|
+
selection.removeAllRanges()
|
|
1589
|
+
selection.addRange(restoredRange)
|
|
1590
|
+
return true
|
|
1591
|
+
}
|
|
1592
|
+
catch {
|
|
1593
|
+
const fallbackText = savedSelectionSnapshot?.text || selectedText
|
|
1594
|
+
return restoreSelectionByTextMatch(fallbackText)
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* 工具条动作触发前先同步一次真实选区,避免点击按钮后浏览器把正文选区清掉。
|
|
1600
|
+
*/
|
|
1601
|
+
function syncSelectionForToolbarAction(): boolean {
|
|
1602
|
+
const previewSelection = readPreviewSelectionState()
|
|
1603
|
+
if (previewSelection) {
|
|
1604
|
+
rememberPreviewSelection(previewSelection.range)
|
|
1605
|
+
selectedText = previewSelection.text
|
|
1606
|
+
return true
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
return restoreSavedPreviewSelection()
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* 所有关闭动作都走同一个 timer,方便菜单 hover、输入框 focus 等场景随时撤销关闭。
|
|
1614
|
+
*/
|
|
1011
1615
|
function clearPendingSelectionToolbarClose(): void {
|
|
1012
1616
|
if (typeof window === 'undefined' || !selectionToolbarCloseTimer) {
|
|
1013
1617
|
return
|
|
@@ -1017,6 +1621,10 @@ ${safeCode}
|
|
|
1017
1621
|
selectionToolbarCloseTimer = undefined
|
|
1018
1622
|
}
|
|
1019
1623
|
|
|
1624
|
+
/**
|
|
1625
|
+
* 关闭工具条刻意延后一小拍,给菜单 hover、输入框 focus 和选区回流留出时间。
|
|
1626
|
+
* 只要子菜单还开着,或焦点仍在 chrome 里,就继续保留当前工具条。
|
|
1627
|
+
*/
|
|
1020
1628
|
function scheduleSelectionToolbarClose(): void {
|
|
1021
1629
|
if (typeof window === 'undefined') {
|
|
1022
1630
|
closeSelectionToolbar()
|
|
@@ -1028,15 +1636,26 @@ ${safeCode}
|
|
|
1028
1636
|
return
|
|
1029
1637
|
}
|
|
1030
1638
|
|
|
1639
|
+
if (activeSelectionMenu) {
|
|
1640
|
+
return
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1031
1643
|
clearPendingSelectionToolbarClose()
|
|
1032
1644
|
selectionToolbarCloseTimer = window.setTimeout(() => {
|
|
1033
1645
|
selectionToolbarCloseTimer = undefined
|
|
1646
|
+
if (activeSelectionMenu || isSelectionChromeFocused()) {
|
|
1647
|
+
return
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1034
1650
|
if (!readPreviewSelectionState()) {
|
|
1035
1651
|
closeSelectionToolbar()
|
|
1036
1652
|
}
|
|
1037
|
-
},
|
|
1653
|
+
}, 600)
|
|
1038
1654
|
}
|
|
1039
1655
|
|
|
1656
|
+
/**
|
|
1657
|
+
* 选区变化可能在一帧内收到多次事件,这里统一合并,避免定位和状态读取来回抖动。
|
|
1658
|
+
*/
|
|
1040
1659
|
function queueSelectionRefresh(remeasured = false): void {
|
|
1041
1660
|
if (typeof window === 'undefined') {
|
|
1042
1661
|
return
|
|
@@ -1055,6 +1674,9 @@ ${safeCode}
|
|
|
1055
1674
|
})
|
|
1056
1675
|
}
|
|
1057
1676
|
|
|
1677
|
+
/**
|
|
1678
|
+
* 子菜单首次打开前拿不到真实宽度,先给一个稳定的经验值参与贴边计算。
|
|
1679
|
+
*/
|
|
1058
1680
|
function estimateSelectionMenuWidth(menu: SelectionMenuKind): number {
|
|
1059
1681
|
if (menu === 'color' || menu === 'link') {
|
|
1060
1682
|
return 256
|
|
@@ -1067,6 +1689,9 @@ ${safeCode}
|
|
|
1067
1689
|
return 288
|
|
1068
1690
|
}
|
|
1069
1691
|
|
|
1692
|
+
/**
|
|
1693
|
+
* 下拉面板以触发按钮为锚点,但最终位置要限制在工具条宽度内,避免菜单越界裁切。
|
|
1694
|
+
*/
|
|
1070
1695
|
function updateSelectionMenuAnchor(menu: SelectionMenuKind): void {
|
|
1071
1696
|
if (!selectionToolbarEl || !activeSelectionMenuTrigger) {
|
|
1072
1697
|
selectionMenuLeft = 0
|
|
@@ -1104,6 +1729,9 @@ ${safeCode}
|
|
|
1104
1729
|
selectionMenuAlignment = 'center'
|
|
1105
1730
|
}
|
|
1106
1731
|
|
|
1732
|
+
/**
|
|
1733
|
+
* 工具条初次出现时尺寸还没稳定,下一帧再量一次可以避免首帧贴边位置偏差。
|
|
1734
|
+
*/
|
|
1107
1735
|
function scheduleSelectionToolbarMeasurement(): void {
|
|
1108
1736
|
if (
|
|
1109
1737
|
typeof window === 'undefined'
|
|
@@ -1122,7 +1750,11 @@ ${safeCode}
|
|
|
1122
1750
|
})
|
|
1123
1751
|
}
|
|
1124
1752
|
|
|
1753
|
+
/**
|
|
1754
|
+
* 子菜单统一从这里开关,顺手记录触发按钮和链接初始值,保证定位与默认值一致。
|
|
1755
|
+
*/
|
|
1125
1756
|
function toggleSelectionMenu(menu: SelectionMenuKind, event?: MouseEvent): void {
|
|
1757
|
+
clearPendingSelectionToolbarClose()
|
|
1126
1758
|
const opening = activeSelectionMenu !== menu
|
|
1127
1759
|
activeSelectionMenu = opening ? menu : null
|
|
1128
1760
|
activeSelectionMenuTrigger = opening && event?.currentTarget instanceof HTMLElement
|
|
@@ -1136,6 +1768,9 @@ ${safeCode}
|
|
|
1136
1768
|
}
|
|
1137
1769
|
}
|
|
1138
1770
|
|
|
1771
|
+
/**
|
|
1772
|
+
* 关闭时把选区相关缓存一并清干净,避免下一次打开还误用上一次的链接和格式状态。
|
|
1773
|
+
*/
|
|
1139
1774
|
function closeSelectionToolbar(): void {
|
|
1140
1775
|
clearPendingSelectionToolbarClose()
|
|
1141
1776
|
selectionToolbarVisible = false
|
|
@@ -1145,9 +1780,249 @@ ${safeCode}
|
|
|
1145
1780
|
selectionMenuAlignment = 'center'
|
|
1146
1781
|
selectedText = ''
|
|
1147
1782
|
linkDraft = ''
|
|
1783
|
+
savedSelectionRange = null
|
|
1784
|
+
savedSelectionSnapshot = null
|
|
1148
1785
|
selectionFormatState = { ...DEFAULT_SELECTION_FORMAT_STATE }
|
|
1149
1786
|
}
|
|
1150
1787
|
|
|
1788
|
+
/**
|
|
1789
|
+
* 面板上的按钮点击不应让浏览器先把正文选区清掉。
|
|
1790
|
+
* 但输入框要保留原生 focus 行为,方便直接编辑链接。
|
|
1791
|
+
*/
|
|
1792
|
+
function handleSelectionPanelMouseDown(event: MouseEvent): void {
|
|
1793
|
+
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
1794
|
+
return
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
event.preventDefault()
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function clearPendingHoveredLinkHide(): void {
|
|
1801
|
+
if (typeof window === 'undefined' || !hoveredLinkHideTimer) {
|
|
1802
|
+
return
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
window.clearTimeout(hoveredLinkHideTimer)
|
|
1806
|
+
hoveredLinkHideTimer = undefined
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
function closeHoveredLink(): void {
|
|
1810
|
+
clearPendingHoveredLinkHide()
|
|
1811
|
+
hoveredLinkState = null
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function scheduleHoveredLinkHide(): void {
|
|
1815
|
+
if (typeof window === 'undefined') {
|
|
1816
|
+
closeHoveredLink()
|
|
1817
|
+
return
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
clearPendingHoveredLinkHide()
|
|
1821
|
+
hoveredLinkHideTimer = window.setTimeout(() => {
|
|
1822
|
+
hoveredLinkHideTimer = undefined
|
|
1823
|
+
hoveredLinkState = null
|
|
1824
|
+
}, 140)
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function resolveHoveredLinkTargets(
|
|
1828
|
+
link: HTMLAnchorElement,
|
|
1829
|
+
): { href: string, rawHref: string } | null {
|
|
1830
|
+
const rawHref = link.getAttribute('href')?.trim() ?? ''
|
|
1831
|
+
const href = link.href.trim() || rawHref
|
|
1832
|
+
if (!href) {
|
|
1833
|
+
return null
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
return {
|
|
1837
|
+
href,
|
|
1838
|
+
rawHref: rawHref || href,
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function measureHoveredLinkPopover(): { width: number, height: number } {
|
|
1843
|
+
const width = hoveredLinkPopoverEl
|
|
1844
|
+
? Math.max(
|
|
1845
|
+
hoveredLinkPopoverEl.offsetWidth,
|
|
1846
|
+
hoveredLinkPopoverEl.clientWidth,
|
|
1847
|
+
)
|
|
1848
|
+
: 92
|
|
1849
|
+
const height = hoveredLinkPopoverEl
|
|
1850
|
+
? Math.max(
|
|
1851
|
+
hoveredLinkPopoverEl.offsetHeight,
|
|
1852
|
+
hoveredLinkPopoverEl.clientHeight,
|
|
1853
|
+
)
|
|
1854
|
+
: 40
|
|
1855
|
+
|
|
1856
|
+
return { width, height }
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function buildHoveredLinkState(
|
|
1860
|
+
link: HTMLAnchorElement,
|
|
1861
|
+
): HoveredLinkState | null {
|
|
1862
|
+
if (!editorScrollHost || !previewHost || !previewHost.contains(link)) {
|
|
1863
|
+
return null
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
const targets = resolveHoveredLinkTargets(link)
|
|
1867
|
+
if (!targets) {
|
|
1868
|
+
return null
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
const hostRect = editorScrollHost.getBoundingClientRect()
|
|
1872
|
+
const linkRect = link.getBoundingClientRect()
|
|
1873
|
+
const { width, height } = measureHoveredLinkPopover()
|
|
1874
|
+
const offset = 8
|
|
1875
|
+
const horizontalPadding = 16
|
|
1876
|
+
const topSpace = linkRect.top - hostRect.top
|
|
1877
|
+
const bottomSpace = hostRect.bottom - linkRect.bottom
|
|
1878
|
+
const placement: HoveredLinkState['placement']
|
|
1879
|
+
= topSpace >= height + offset || bottomSpace < height + offset
|
|
1880
|
+
? 'top'
|
|
1881
|
+
: 'bottom'
|
|
1882
|
+
const center = linkRect.left - hostRect.left + linkRect.width / 2
|
|
1883
|
+
const maxLeft = Math.max(
|
|
1884
|
+
horizontalPadding,
|
|
1885
|
+
editorScrollHost.clientWidth - horizontalPadding - width,
|
|
1886
|
+
)
|
|
1887
|
+
const left = Math.min(
|
|
1888
|
+
Math.max(center - width / 2, horizontalPadding),
|
|
1889
|
+
maxLeft,
|
|
1890
|
+
)
|
|
1891
|
+
const top = placement === 'top'
|
|
1892
|
+
? editorScrollHost.scrollTop + linkRect.top - hostRect.top - height - offset
|
|
1893
|
+
: editorScrollHost.scrollTop + linkRect.bottom - hostRect.top + offset
|
|
1894
|
+
|
|
1895
|
+
return {
|
|
1896
|
+
element: link,
|
|
1897
|
+
href: targets.href,
|
|
1898
|
+
rawHref: targets.rawHref,
|
|
1899
|
+
top: Math.max(8, top),
|
|
1900
|
+
left,
|
|
1901
|
+
placement,
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function syncHoveredLinkPosition(): void {
|
|
1906
|
+
if (!hoveredLinkState) {
|
|
1907
|
+
return
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
if (!hoveredLinkState.element.isConnected) {
|
|
1911
|
+
closeHoveredLink()
|
|
1912
|
+
return
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
const nextState = buildHoveredLinkState(hoveredLinkState.element)
|
|
1916
|
+
if (!nextState) {
|
|
1917
|
+
closeHoveredLink()
|
|
1918
|
+
return
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
hoveredLinkState = nextState
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function resolveColorOptionValue(colorOption: string | ColorSwatchOption): string | null {
|
|
1925
|
+
return typeof colorOption === 'string' ? colorOption : colorOption.value
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function updateHoveredLink(link: HTMLAnchorElement | null): void {
|
|
1929
|
+
if (!editable || !onapplylink || selectionToolbarVisible) {
|
|
1930
|
+
closeHoveredLink()
|
|
1931
|
+
return
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
if (!link) {
|
|
1935
|
+
scheduleHoveredLinkHide()
|
|
1936
|
+
return
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
const nextState = buildHoveredLinkState(link)
|
|
1940
|
+
if (!nextState) {
|
|
1941
|
+
scheduleHoveredLinkHide()
|
|
1942
|
+
return
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
clearPendingHoveredLinkHide()
|
|
1946
|
+
hoveredLinkState = nextState
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
function openHoveredLink(): void {
|
|
1950
|
+
if (typeof window === 'undefined' || !hoveredLinkState?.href) {
|
|
1951
|
+
return
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
window.open(
|
|
1955
|
+
hoveredLinkState.href,
|
|
1956
|
+
hoveredLinkState.element.target || '_blank',
|
|
1957
|
+
'noopener,noreferrer',
|
|
1958
|
+
)
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function createRangeForLinkText(link: HTMLAnchorElement): Range {
|
|
1962
|
+
const range = document.createRange()
|
|
1963
|
+
const walker = document.createTreeWalker(link, NodeFilter.SHOW_TEXT)
|
|
1964
|
+
let current = walker.nextNode()
|
|
1965
|
+
let firstTextNode: Node | null = null
|
|
1966
|
+
let lastTextNode: Node | null = null
|
|
1967
|
+
|
|
1968
|
+
while (current) {
|
|
1969
|
+
if ((current.textContent ?? '').length > 0) {
|
|
1970
|
+
firstTextNode ??= current
|
|
1971
|
+
lastTextNode = current
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
current = walker.nextNode()
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
if (!firstTextNode || !lastTextNode) {
|
|
1978
|
+
range.selectNodeContents(link)
|
|
1979
|
+
return range
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
range.setStart(firstTextNode, 0)
|
|
1983
|
+
range.setEnd(lastTextNode, lastTextNode.textContent?.length ?? 0)
|
|
1984
|
+
return range
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
async function editHoveredLink(): Promise<void> {
|
|
1988
|
+
if (
|
|
1989
|
+
typeof window === 'undefined'
|
|
1990
|
+
|| !previewHost
|
|
1991
|
+
|| !onapplylink
|
|
1992
|
+
|| !hoveredLinkState
|
|
1993
|
+
) {
|
|
1994
|
+
return
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
const link = hoveredLinkState.element
|
|
1998
|
+
const linkHref = hoveredLinkState.rawHref
|
|
1999
|
+
if (!previewHost.contains(link)) {
|
|
2000
|
+
closeHoveredLink()
|
|
2001
|
+
return
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
const selection = window.getSelection()
|
|
2005
|
+
if (!selection) {
|
|
2006
|
+
return
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
const range = createRangeForLinkText(link)
|
|
2010
|
+
previewHost.focus()
|
|
2011
|
+
selection.removeAllRanges()
|
|
2012
|
+
selection.addRange(range)
|
|
2013
|
+
rememberPreviewSelection(range)
|
|
2014
|
+
selectedText = range.toString().trim()
|
|
2015
|
+
closeHoveredLink()
|
|
2016
|
+
handleSelectionChange(true)
|
|
2017
|
+
await tick()
|
|
2018
|
+
linkDraft = linkHref || 'https://'
|
|
2019
|
+
activeSelectionMenu = 'link'
|
|
2020
|
+
activeSelectionMenuTrigger = selectionLinkButtonEl
|
|
2021
|
+
if (selectionLinkButtonEl) {
|
|
2022
|
+
updateSelectionMenuAnchor('link')
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1151
2026
|
/**
|
|
1152
2027
|
* 目录高亮不依赖 IntersectionObserver,而是按滚动容器内标题的相对位置推断。
|
|
1153
2028
|
* 这样正文持续流式刷新时不需要反复重建 observer,也更容易和目录点击滚动保持一致。
|
|
@@ -1211,6 +2086,7 @@ ${safeCode}
|
|
|
1211
2086
|
function handleDocumentScroll(event: Event): void {
|
|
1212
2087
|
syncActiveHeadingFromScroll()
|
|
1213
2088
|
syncPinnedTitleVisibility()
|
|
2089
|
+
syncHoveredLinkPosition()
|
|
1214
2090
|
if (selectionToolbarVisible) {
|
|
1215
2091
|
queueSelectionRefresh(true)
|
|
1216
2092
|
}
|
|
@@ -1250,11 +2126,16 @@ ${safeCode}
|
|
|
1250
2126
|
|
|
1251
2127
|
const previewSelection = readPreviewSelectionState()
|
|
1252
2128
|
if (!previewSelection) {
|
|
2129
|
+
if (savedSelectionRange && isSelectionChromeFocused()) {
|
|
2130
|
+
return
|
|
2131
|
+
}
|
|
2132
|
+
|
|
1253
2133
|
scheduleSelectionToolbarClose()
|
|
1254
2134
|
return
|
|
1255
2135
|
}
|
|
1256
2136
|
|
|
1257
2137
|
const { selection, range, text } = previewSelection
|
|
2138
|
+
hoveredLinkState = null
|
|
1258
2139
|
const rect = range.getBoundingClientRect()
|
|
1259
2140
|
const hostRect = editorScrollHost.getBoundingClientRect()
|
|
1260
2141
|
// 浮层尺寸在首次渲染前拿不到精确高度,这里用稳定的经验值决定上下避让方向,避免直接盖住选中文本。
|
|
@@ -1269,31 +2150,47 @@ ${safeCode}
|
|
|
1269
2150
|
: 'bottom'
|
|
1270
2151
|
const center = rect.left - hostRect.left + rect.width / 2
|
|
1271
2152
|
const horizontalPadding = 16
|
|
1272
|
-
const
|
|
1273
|
-
?
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
2153
|
+
const outlineOverlap = outlinePanelEl
|
|
2154
|
+
? Math.max(
|
|
2155
|
+
0,
|
|
2156
|
+
Math.min(
|
|
2157
|
+
outlinePanelEl.getBoundingClientRect().right,
|
|
2158
|
+
hostRect.right,
|
|
2159
|
+
) - hostRect.left,
|
|
2160
|
+
)
|
|
2161
|
+
: 0
|
|
2162
|
+
const minLeft = Math.max(
|
|
2163
|
+
horizontalPadding,
|
|
2164
|
+
outlineOverlap > 24 ? outlineOverlap + 12 : horizontalPadding,
|
|
2165
|
+
)
|
|
2166
|
+
const measuredToolbarWidth = selectionChromeEl
|
|
2167
|
+
? Math.max(selectionChromeEl.offsetWidth, selectionChromeEl.clientWidth)
|
|
2168
|
+
: selectionToolbarEl
|
|
2169
|
+
? Math.max(selectionToolbarEl.offsetWidth, selectionToolbarEl.clientWidth)
|
|
2170
|
+
: 360
|
|
2171
|
+
const maxToolbarWidth = Math.max(
|
|
2172
|
+
220,
|
|
2173
|
+
editorScrollHost.clientWidth - minLeft - horizontalPadding,
|
|
1278
2174
|
)
|
|
1279
|
-
const
|
|
1280
|
-
const
|
|
2175
|
+
const toolbarWidth = Math.min(measuredToolbarWidth, maxToolbarWidth)
|
|
2176
|
+
const maxLeft = Math.max(
|
|
2177
|
+
minLeft,
|
|
2178
|
+
editorScrollHost.clientWidth - horizontalPadding - toolbarWidth,
|
|
2179
|
+
)
|
|
2180
|
+
const unclampedLeft = center - toolbarWidth / 2
|
|
2181
|
+
const toolbarLeft = Math.min(Math.max(unclampedLeft, minLeft), maxLeft)
|
|
1281
2182
|
const alignment: SelectionToolbarPosition['alignment']
|
|
1282
|
-
=
|
|
2183
|
+
= toolbarLeft <= minLeft + 0.5
|
|
1283
2184
|
? 'left'
|
|
1284
|
-
:
|
|
2185
|
+
: toolbarLeft >= maxLeft - 0.5
|
|
1285
2186
|
? 'right'
|
|
1286
2187
|
: 'center'
|
|
1287
|
-
const toolbarLeft = alignment === 'left'
|
|
1288
|
-
? horizontalPadding
|
|
1289
|
-
: alignment === 'right'
|
|
1290
|
-
? editorScrollHost.clientWidth - horizontalPadding
|
|
1291
|
-
: center
|
|
1292
2188
|
const toolbarTop = placement === 'top'
|
|
1293
2189
|
? editorScrollHost.scrollTop + rect.top - hostRect.top - estimatedToolbarHeight - offset
|
|
1294
2190
|
: editorScrollHost.scrollTop + rect.bottom - hostRect.top + offset
|
|
1295
2191
|
|
|
1296
2192
|
selectedText = text
|
|
2193
|
+
rememberPreviewSelection(range)
|
|
1297
2194
|
selectionToolbarVisible = true
|
|
1298
2195
|
selectionFormatState = readSelectionFormatState(selection)
|
|
1299
2196
|
toolbarPosition = {
|
|
@@ -1328,6 +2225,10 @@ ${safeCode}
|
|
|
1328
2225
|
}
|
|
1329
2226
|
|
|
1330
2227
|
function applyBlockFormat(kind: MarkdownBlockFormatKind): void {
|
|
2228
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2229
|
+
return
|
|
2230
|
+
}
|
|
2231
|
+
|
|
1331
2232
|
onapplyblockformat?.(kind)
|
|
1332
2233
|
activeSelectionMenu = null
|
|
1333
2234
|
activeSelectionMenuTrigger = null
|
|
@@ -1335,66 +2236,160 @@ ${safeCode}
|
|
|
1335
2236
|
}
|
|
1336
2237
|
|
|
1337
2238
|
function applyBlockStyle(kind: MarkdownBlockStyleKind): void {
|
|
2239
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2240
|
+
return
|
|
2241
|
+
}
|
|
2242
|
+
|
|
1338
2243
|
onapplyblockstyle?.(kind)
|
|
1339
2244
|
activeSelectionMenu = null
|
|
1340
2245
|
activeSelectionMenuTrigger = null
|
|
1341
2246
|
queueSelectionRefresh()
|
|
1342
2247
|
}
|
|
1343
2248
|
|
|
2249
|
+
/**
|
|
2250
|
+
* 行内样式按钮允许连续点按,因此这里不主动关掉工具条,只刷新状态。
|
|
2251
|
+
*/
|
|
1344
2252
|
function applyInlineFormat(kind: MarkdownInlineFormatKind): void {
|
|
2253
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2254
|
+
return
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
clearPendingSelectionToolbarClose()
|
|
1345
2258
|
onapplyinlineformat?.(kind)
|
|
1346
2259
|
queueSelectionRefresh()
|
|
1347
2260
|
}
|
|
1348
2261
|
|
|
1349
2262
|
function applyAlignment(kind: MarkdownTextAlignKind): void {
|
|
2263
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2264
|
+
return
|
|
2265
|
+
}
|
|
2266
|
+
|
|
1350
2267
|
onapplyalignment?.(kind)
|
|
1351
2268
|
activeSelectionMenu = null
|
|
1352
2269
|
activeSelectionMenuTrigger = null
|
|
1353
2270
|
queueSelectionRefresh()
|
|
1354
2271
|
}
|
|
1355
2272
|
|
|
2273
|
+
/**
|
|
2274
|
+
* 链接菜单提交时总是以恢复后的正文选区为准,避免输入框焦点把目标文本丢掉。
|
|
2275
|
+
*/
|
|
1356
2276
|
function applyLink(): void {
|
|
1357
|
-
|
|
2277
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2278
|
+
return
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
onapplylink?.(
|
|
2282
|
+
linkDraft.trim() || null,
|
|
2283
|
+
savedSelectionSnapshot
|
|
2284
|
+
? {
|
|
2285
|
+
...savedSelectionSnapshot,
|
|
2286
|
+
}
|
|
2287
|
+
: undefined,
|
|
2288
|
+
selectedText.trim() || undefined,
|
|
2289
|
+
)
|
|
1358
2290
|
activeSelectionMenu = null
|
|
1359
2291
|
activeSelectionMenuTrigger = null
|
|
1360
2292
|
queueSelectionRefresh()
|
|
1361
2293
|
}
|
|
1362
2294
|
|
|
1363
2295
|
function removeLink(): void {
|
|
1364
|
-
|
|
2296
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2297
|
+
return
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
onapplylink?.(
|
|
2301
|
+
null,
|
|
2302
|
+
savedSelectionSnapshot
|
|
2303
|
+
? {
|
|
2304
|
+
...savedSelectionSnapshot,
|
|
2305
|
+
}
|
|
2306
|
+
: undefined,
|
|
2307
|
+
selectedText.trim() || undefined,
|
|
2308
|
+
)
|
|
1365
2309
|
activeSelectionMenu = null
|
|
1366
2310
|
activeSelectionMenuTrigger = null
|
|
1367
2311
|
queueSelectionRefresh()
|
|
1368
2312
|
}
|
|
1369
2313
|
|
|
2314
|
+
/**
|
|
2315
|
+
* 颜色动作除了颜色值,还会把当前选中文本一起带给外层,供宿主侧做兜底选区恢复。
|
|
2316
|
+
*/
|
|
1370
2317
|
function applyColor(request: MarkdownColorFormatRequest): void {
|
|
1371
|
-
|
|
2318
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2319
|
+
return
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
onapplycolor?.({
|
|
2323
|
+
...request,
|
|
2324
|
+
selectedText: selectedText.trim() || undefined,
|
|
2325
|
+
selectionSnapshot: savedSelectionSnapshot
|
|
2326
|
+
? {
|
|
2327
|
+
...savedSelectionSnapshot,
|
|
2328
|
+
}
|
|
2329
|
+
: undefined,
|
|
2330
|
+
})
|
|
1372
2331
|
queueSelectionRefresh()
|
|
1373
2332
|
}
|
|
1374
2333
|
|
|
1375
2334
|
async function copySelection(): Promise<void> {
|
|
2335
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2336
|
+
return
|
|
2337
|
+
}
|
|
2338
|
+
|
|
1376
2339
|
await oncopyselection?.()
|
|
1377
2340
|
activeSelectionMenu = null
|
|
1378
2341
|
activeSelectionMenuTrigger = null
|
|
1379
2342
|
}
|
|
1380
2343
|
|
|
1381
2344
|
async function annotateSelection(): Promise<void> {
|
|
2345
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2346
|
+
return
|
|
2347
|
+
}
|
|
2348
|
+
|
|
1382
2349
|
await onannotation?.()
|
|
1383
2350
|
activeSelectionMenu = null
|
|
1384
2351
|
activeSelectionMenuTrigger = null
|
|
1385
2352
|
}
|
|
1386
2353
|
|
|
2354
|
+
function handlePreviewMouseMove(event: MouseEvent): void {
|
|
2355
|
+
const target = event.target
|
|
2356
|
+
const link = target instanceof Element
|
|
2357
|
+
? target.closest<HTMLAnchorElement>('a[href]')
|
|
2358
|
+
: null
|
|
2359
|
+
|
|
2360
|
+
updateHoveredLink(link && previewHost?.contains(link) ? link : null)
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
function handlePreviewMouseLeave(): void {
|
|
2364
|
+
scheduleHoveredLinkHide()
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
/**
|
|
2368
|
+
* mouseup 比 selectionchange 更接近用户“完成一次选中”的时机,适合触发工具条刷新。
|
|
2369
|
+
*/
|
|
1387
2370
|
function handlePreviewMouseUp(): void {
|
|
1388
2371
|
queueSelectionRefresh()
|
|
1389
2372
|
onpreviewmouseup?.()
|
|
1390
2373
|
}
|
|
1391
2374
|
|
|
2375
|
+
/**
|
|
2376
|
+
* 输入事件本身只透传给宿主;选区状态交给 selectionchange/mouseup 统一管理。
|
|
2377
|
+
*/
|
|
1392
2378
|
function handlePreviewInput(event: Event): void {
|
|
1393
2379
|
onpreviewinput?.(event)
|
|
1394
2380
|
}
|
|
1395
2381
|
|
|
1396
|
-
|
|
2382
|
+
/**
|
|
2383
|
+
* 正文失焦时先观察焦点是否转移到工具条内部,避免点击链接输入框时误触发提交或收起。
|
|
2384
|
+
*/
|
|
2385
|
+
function handlePreviewBlur(event: FocusEvent): void {
|
|
1397
2386
|
queueSelectionRefresh(true)
|
|
2387
|
+
|
|
2388
|
+
const nextTarget = event.relatedTarget
|
|
2389
|
+
if (nextTarget instanceof Node && selectionChromeEl?.contains(nextTarget)) {
|
|
2390
|
+
return
|
|
2391
|
+
}
|
|
2392
|
+
|
|
1398
2393
|
onpreviewblur?.()
|
|
1399
2394
|
}
|
|
1400
2395
|
</script>
|
|
@@ -1539,11 +2534,11 @@ ${safeCode}
|
|
|
1539
2534
|
{/if}
|
|
1540
2535
|
|
|
1541
2536
|
<div
|
|
1542
|
-
class:hai-ai-doc-layout-collapsed={outlineCollapsed}
|
|
2537
|
+
class:hai-ai-doc-layout-collapsed={!showOutline || outlineCollapsed}
|
|
1543
2538
|
class='hai-ai-doc-layout'
|
|
1544
2539
|
>
|
|
1545
2540
|
{#if showOutline && !outlineCollapsed}
|
|
1546
|
-
<aside class='hai-ai-doc-outline'>
|
|
2541
|
+
<aside bind:this={outlinePanelEl} class='hai-ai-doc-outline'>
|
|
1547
2542
|
<div class='hai-ai-doc-outline-head'>
|
|
1548
2543
|
<strong>{uiM('markdown_outline')}</strong>
|
|
1549
2544
|
|
|
@@ -1599,7 +2594,10 @@ ${safeCode}
|
|
|
1599
2594
|
style={`top:${toolbarPosition.top}px; left:${toolbarPosition.left}px;`}
|
|
1600
2595
|
>
|
|
1601
2596
|
<div
|
|
2597
|
+
bind:this={selectionChromeEl}
|
|
1602
2598
|
class='hai-ai-doc-selection-chrome'
|
|
2599
|
+
onmouseenter={clearPendingSelectionToolbarClose}
|
|
2600
|
+
onmouseleave={() => scheduleSelectionToolbarClose()}
|
|
1603
2601
|
>
|
|
1604
2602
|
<div
|
|
1605
2603
|
bind:this={selectionToolbarEl}
|
|
@@ -1613,12 +2611,19 @@ ${safeCode}
|
|
|
1613
2611
|
type='button'
|
|
1614
2612
|
class={cn(
|
|
1615
2613
|
'hai-ai-doc-selection-chip',
|
|
1616
|
-
activeSelectionMenu === 'rewrite'
|
|
2614
|
+
(!directRewriteAction && activeSelectionMenu === 'rewrite')
|
|
1617
2615
|
? 'hai-ai-doc-selection-chip--active'
|
|
1618
2616
|
: '',
|
|
1619
2617
|
)}
|
|
1620
2618
|
disabled={rewritePending}
|
|
1621
|
-
onclick={event =>
|
|
2619
|
+
onclick={(event) => {
|
|
2620
|
+
if (directRewriteAction) {
|
|
2621
|
+
void applyRewrite(directRewriteAction.id)
|
|
2622
|
+
return
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
toggleSelectionMenu('rewrite', event)
|
|
2626
|
+
}}
|
|
1622
2627
|
>
|
|
1623
2628
|
<svg viewBox='0 0 24 24' aria-hidden='true'>
|
|
1624
2629
|
<path
|
|
@@ -1756,7 +2761,7 @@ ${safeCode}
|
|
|
1756
2761
|
title={uiM('markdown_format_strike')}
|
|
1757
2762
|
onclick={() => applyInlineFormat('strike')}
|
|
1758
2763
|
>
|
|
1759
|
-
S
|
|
2764
|
+
<s>S</s>
|
|
1760
2765
|
</button>
|
|
1761
2766
|
<button
|
|
1762
2767
|
type='button'
|
|
@@ -1782,12 +2787,13 @@ ${safeCode}
|
|
|
1782
2787
|
title={uiM('markdown_format_underline')}
|
|
1783
2788
|
onclick={() => applyInlineFormat('underline')}
|
|
1784
2789
|
>
|
|
1785
|
-
U
|
|
2790
|
+
<u>U</u>
|
|
1786
2791
|
</button>
|
|
1787
2792
|
{/if}
|
|
1788
2793
|
|
|
1789
2794
|
{#if onapplylink || onapplyinlineformat}
|
|
1790
2795
|
<button
|
|
2796
|
+
bind:this={selectionLinkButtonEl}
|
|
1791
2797
|
type='button'
|
|
1792
2798
|
class={cn(
|
|
1793
2799
|
'hai-ai-doc-selection-btn',
|
|
@@ -1804,10 +2810,18 @@ ${safeCode}
|
|
|
1804
2810
|
? toggleSelectionMenu('link', event)
|
|
1805
2811
|
: applyInlineFormat('link')}
|
|
1806
2812
|
>
|
|
1807
|
-
<svg
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
2813
|
+
<svg
|
|
2814
|
+
viewBox='0 0 24 24'
|
|
2815
|
+
aria-hidden='true'
|
|
2816
|
+
fill='none'
|
|
2817
|
+
stroke='currentColor'
|
|
2818
|
+
stroke-width='2'
|
|
2819
|
+
stroke-linecap='round'
|
|
2820
|
+
stroke-linejoin='round'
|
|
2821
|
+
>
|
|
2822
|
+
<path d='M9.5 14.5 14.5 9.5'></path>
|
|
2823
|
+
<path d='M7.6 16.4 6.3 17.7a3 3 0 1 1-4.24-4.24l3-3a3 3 0 0 1 4.24 0'></path>
|
|
2824
|
+
<path d='M16.4 7.6 17.7 6.3a3 3 0 1 1 4.24 4.24l-3 3a3 3 0 0 1-4.24 0'></path>
|
|
1811
2825
|
</svg>
|
|
1812
2826
|
</button>
|
|
1813
2827
|
{/if}
|
|
@@ -1826,19 +2840,6 @@ ${safeCode}
|
|
|
1826
2840
|
>
|
|
1827
2841
|
</>
|
|
1828
2842
|
</button>
|
|
1829
|
-
<button
|
|
1830
|
-
type='button'
|
|
1831
|
-
class={cn(
|
|
1832
|
-
'hai-ai-doc-selection-btn',
|
|
1833
|
-
selectionFormatState.highlight
|
|
1834
|
-
? 'hai-ai-doc-selection-btn--active'
|
|
1835
|
-
: '',
|
|
1836
|
-
)}
|
|
1837
|
-
title={uiM('markdown_format_highlight')}
|
|
1838
|
-
onclick={() => applyInlineFormat('highlight')}
|
|
1839
|
-
>
|
|
1840
|
-
<span class='hai-ai-doc-selection-highlight-icon'>A</span>
|
|
1841
|
-
</button>
|
|
1842
2843
|
{/if}
|
|
1843
2844
|
|
|
1844
2845
|
{#if onapplycolor}
|
|
@@ -1905,14 +2906,14 @@ ${safeCode}
|
|
|
1905
2906
|
{/if}
|
|
1906
2907
|
</div>
|
|
1907
2908
|
|
|
1908
|
-
{#if activeSelectionMenu === 'rewrite' && resolvedRewriteActions.length >
|
|
2909
|
+
{#if activeSelectionMenu === 'rewrite' && resolvedRewriteActions.length > 1}
|
|
1909
2910
|
<div
|
|
1910
2911
|
class='hai-ai-doc-rewrite-menu'
|
|
1911
2912
|
data-menu-alignment={selectionMenuAlignment}
|
|
1912
2913
|
role='menu'
|
|
1913
2914
|
tabindex='-1'
|
|
1914
2915
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
1915
|
-
onmousedown={
|
|
2916
|
+
onmousedown={handleSelectionPanelMouseDown}
|
|
1916
2917
|
>
|
|
1917
2918
|
{#each resolvedRewriteActions as action}
|
|
1918
2919
|
<button
|
|
@@ -1932,7 +2933,7 @@ ${safeCode}
|
|
|
1932
2933
|
role='menu'
|
|
1933
2934
|
tabindex='-1'
|
|
1934
2935
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
1935
|
-
onmousedown={
|
|
2936
|
+
onmousedown={handleSelectionPanelMouseDown}
|
|
1936
2937
|
>
|
|
1937
2938
|
{#each BLOCK_FORMAT_OPTIONS as option}
|
|
1938
2939
|
<button
|
|
@@ -1964,7 +2965,7 @@ ${safeCode}
|
|
|
1964
2965
|
role='menu'
|
|
1965
2966
|
tabindex='-1'
|
|
1966
2967
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
1967
|
-
onmousedown={
|
|
2968
|
+
onmousedown={handleSelectionPanelMouseDown}
|
|
1968
2969
|
>
|
|
1969
2970
|
{#each ALIGN_OPTIONS as option}
|
|
1970
2971
|
<button
|
|
@@ -1992,9 +2993,10 @@ ${safeCode}
|
|
|
1992
2993
|
aria-label={uiM('markdown_link_dialog_label')}
|
|
1993
2994
|
tabindex='-1'
|
|
1994
2995
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
1995
|
-
onmousedown={
|
|
2996
|
+
onmousedown={handleSelectionPanelMouseDown}
|
|
1996
2997
|
>
|
|
1997
2998
|
<input
|
|
2999
|
+
bind:this={linkInputEl}
|
|
1998
3000
|
class='hai-ai-doc-selection-input'
|
|
1999
3001
|
bind:value={linkDraft}
|
|
2000
3002
|
aria-label={uiM('markdown_link_input_label')}
|
|
@@ -2032,7 +3034,7 @@ ${safeCode}
|
|
|
2032
3034
|
aria-label={uiM('markdown_color_dialog_label')}
|
|
2033
3035
|
tabindex='-1'
|
|
2034
3036
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
2035
|
-
onmousedown={
|
|
3037
|
+
onmousedown={handleSelectionPanelMouseDown}
|
|
2036
3038
|
>
|
|
2037
3039
|
<div class='hai-ai-doc-color-group'>
|
|
2038
3040
|
<div class='hai-ai-doc-color-group-title'>
|
|
@@ -2062,23 +3064,34 @@ ${safeCode}
|
|
|
2062
3064
|
{uiM('markdown_background_color')}
|
|
2063
3065
|
</div>
|
|
2064
3066
|
<div class='hai-ai-doc-color-grid'>
|
|
2065
|
-
{#each
|
|
3067
|
+
{#each BACKGROUND_COLOR_OPTIONS as colorOption}
|
|
2066
3068
|
<button
|
|
2067
3069
|
type='button'
|
|
2068
3070
|
class={cn(
|
|
2069
3071
|
'hai-ai-doc-color-swatch',
|
|
2070
|
-
selectionFormatState.backgroundColor ===
|
|
3072
|
+
selectionFormatState.backgroundColor === resolveColorOptionValue(colorOption)
|
|
2071
3073
|
? 'hai-ai-doc-color-swatch--active'
|
|
2072
3074
|
: '',
|
|
2073
3075
|
)}
|
|
2074
|
-
|
|
3076
|
+
data-empty={typeof colorOption === 'string'
|
|
3077
|
+
? undefined
|
|
3078
|
+
: 'true'}
|
|
3079
|
+
title={typeof colorOption === 'string'
|
|
3080
|
+
? colorOption
|
|
3081
|
+
: uiM(colorOption.titleKey)}
|
|
2075
3082
|
onclick={() =>
|
|
2076
3083
|
applyColor({
|
|
2077
3084
|
target: 'background',
|
|
2078
|
-
value:
|
|
3085
|
+
value: typeof colorOption === 'string'
|
|
3086
|
+
? colorOption
|
|
3087
|
+
: colorOption.value,
|
|
2079
3088
|
})}
|
|
2080
3089
|
>
|
|
2081
|
-
<span
|
|
3090
|
+
<span
|
|
3091
|
+
style={typeof colorOption === 'string'
|
|
3092
|
+
? `background:${colorOption};`
|
|
3093
|
+
: undefined}
|
|
3094
|
+
></span>
|
|
2082
3095
|
</button>
|
|
2083
3096
|
{/each}
|
|
2084
3097
|
</div>
|
|
@@ -2099,11 +3112,87 @@ ${safeCode}
|
|
|
2099
3112
|
</div>
|
|
2100
3113
|
{/if}
|
|
2101
3114
|
|
|
3115
|
+
{#if hoveredLinkState}
|
|
3116
|
+
<div
|
|
3117
|
+
bind:this={hoveredLinkPopoverEl}
|
|
3118
|
+
class='hai-ai-doc-link-hover'
|
|
3119
|
+
data-placement={hoveredLinkState.placement}
|
|
3120
|
+
style={`top:${hoveredLinkState.top}px; left:${hoveredLinkState.left}px;`}
|
|
3121
|
+
onmouseenter={clearPendingHoveredLinkHide}
|
|
3122
|
+
onmouseleave={handlePreviewMouseLeave}
|
|
3123
|
+
>
|
|
3124
|
+
<button
|
|
3125
|
+
type='button'
|
|
3126
|
+
class='hai-ai-doc-link-hover-btn'
|
|
3127
|
+
title={uiM('markdown_link_open')}
|
|
3128
|
+
aria-label={uiM('markdown_link_open')}
|
|
3129
|
+
onmousedown={event => event.preventDefault()}
|
|
3130
|
+
onclick={openHoveredLink}
|
|
3131
|
+
>
|
|
3132
|
+
<svg viewBox='0 0 24 24' aria-hidden='true'>
|
|
3133
|
+
<path
|
|
3134
|
+
d='M13.75 5.75h4.5v4.5'
|
|
3135
|
+
fill='none'
|
|
3136
|
+
stroke='currentColor'
|
|
3137
|
+
stroke-width='1.9'
|
|
3138
|
+
stroke-linecap='round'
|
|
3139
|
+
stroke-linejoin='round'
|
|
3140
|
+
></path>
|
|
3141
|
+
<path
|
|
3142
|
+
d='M18 6 11 13'
|
|
3143
|
+
fill='none'
|
|
3144
|
+
stroke='currentColor'
|
|
3145
|
+
stroke-width='1.9'
|
|
3146
|
+
stroke-linecap='round'
|
|
3147
|
+
stroke-linejoin='round'
|
|
3148
|
+
></path>
|
|
3149
|
+
<path
|
|
3150
|
+
d='M10.25 7.75h-2.5A2.5 2.5 0 0 0 5.25 10.25v6A2.5 2.5 0 0 0 7.75 18.75h6a2.5 2.5 0 0 0 2.5-2.5v-2.5'
|
|
3151
|
+
fill='none'
|
|
3152
|
+
stroke='currentColor'
|
|
3153
|
+
stroke-width='1.9'
|
|
3154
|
+
stroke-linecap='round'
|
|
3155
|
+
stroke-linejoin='round'
|
|
3156
|
+
></path>
|
|
3157
|
+
</svg>
|
|
3158
|
+
</button>
|
|
3159
|
+
<button
|
|
3160
|
+
type='button'
|
|
3161
|
+
class='hai-ai-doc-link-hover-btn'
|
|
3162
|
+
title={uiM('markdown_link_edit')}
|
|
3163
|
+
aria-label={uiM('markdown_link_edit')}
|
|
3164
|
+
onmousedown={event => event.preventDefault()}
|
|
3165
|
+
onclick={() => void editHoveredLink()}
|
|
3166
|
+
>
|
|
3167
|
+
<svg viewBox='0 0 24 24' aria-hidden='true'>
|
|
3168
|
+
<path
|
|
3169
|
+
d='M4.75 16.88V19.25h2.37l9.96-9.96-2.37-2.37-9.96 9.96Z'
|
|
3170
|
+
fill='none'
|
|
3171
|
+
stroke='currentColor'
|
|
3172
|
+
stroke-width='1.9'
|
|
3173
|
+
stroke-linecap='round'
|
|
3174
|
+
stroke-linejoin='round'
|
|
3175
|
+
></path>
|
|
3176
|
+
<path
|
|
3177
|
+
d='M13.96 7.92 16.33 5.55a1.68 1.68 0 0 1 2.37 2.37l-2.37 2.37'
|
|
3178
|
+
fill='none'
|
|
3179
|
+
stroke='currentColor'
|
|
3180
|
+
stroke-width='1.9'
|
|
3181
|
+
stroke-linecap='round'
|
|
3182
|
+
stroke-linejoin='round'
|
|
3183
|
+
></path>
|
|
3184
|
+
</svg>
|
|
3185
|
+
</button>
|
|
3186
|
+
</div>
|
|
3187
|
+
{/if}
|
|
3188
|
+
|
|
2102
3189
|
<article
|
|
2103
3190
|
bind:this={previewHost}
|
|
2104
3191
|
class={readerDocumentClass}
|
|
2105
3192
|
contenteditable={editable}
|
|
2106
3193
|
role='document'
|
|
3194
|
+
onmousemove={handlePreviewMouseMove}
|
|
3195
|
+
onmouseleave={handlePreviewMouseLeave}
|
|
2107
3196
|
onmouseup={handlePreviewMouseUp}
|
|
2108
3197
|
oninput={handlePreviewInput}
|
|
2109
3198
|
onblur={handlePreviewBlur}
|
|
@@ -2126,8 +3215,12 @@ ${safeCode}
|
|
|
2126
3215
|
);
|
|
2127
3216
|
--hai-ai-doc-selection-fg: oklch(var(--bc, 0.22 0 0));
|
|
2128
3217
|
display: flex;
|
|
3218
|
+
width: 100%;
|
|
3219
|
+
min-width: 0;
|
|
2129
3220
|
min-height: 0;
|
|
2130
3221
|
height: 100%;
|
|
3222
|
+
container-type: inline-size;
|
|
3223
|
+
container-name: ai-doc-editor;
|
|
2131
3224
|
flex-direction: column;
|
|
2132
3225
|
border: 1px solid oklch(var(--bc) / 0.08);
|
|
2133
3226
|
border-radius: 1.5rem;
|
|
@@ -2141,8 +3234,10 @@ ${safeCode}
|
|
|
2141
3234
|
}
|
|
2142
3235
|
|
|
2143
3236
|
.hai-ai-doc-shell {
|
|
2144
|
-
display:
|
|
2145
|
-
|
|
3237
|
+
display: flex;
|
|
3238
|
+
flex-direction: column;
|
|
3239
|
+
width: 100%;
|
|
3240
|
+
min-width: 0;
|
|
2146
3241
|
min-height: 0;
|
|
2147
3242
|
height: 100%;
|
|
2148
3243
|
}
|
|
@@ -2336,18 +3431,23 @@ ${safeCode}
|
|
|
2336
3431
|
}
|
|
2337
3432
|
|
|
2338
3433
|
.hai-ai-doc-layout {
|
|
3434
|
+
width: 100%;
|
|
2339
3435
|
min-height: 0;
|
|
2340
|
-
|
|
2341
|
-
|
|
3436
|
+
min-width: 0;
|
|
3437
|
+
flex: 1 1 auto;
|
|
3438
|
+
display: flex;
|
|
3439
|
+
align-items: stretch;
|
|
2342
3440
|
position: relative;
|
|
2343
3441
|
overflow: hidden;
|
|
2344
3442
|
}
|
|
2345
3443
|
|
|
2346
3444
|
.hai-ai-doc-layout.hai-ai-doc-layout-collapsed {
|
|
2347
|
-
|
|
3445
|
+
display: flex;
|
|
2348
3446
|
}
|
|
2349
3447
|
|
|
2350
3448
|
.hai-ai-doc-outline {
|
|
3449
|
+
width: 220px;
|
|
3450
|
+
flex: 0 0 220px;
|
|
2351
3451
|
min-height: 0;
|
|
2352
3452
|
display: grid;
|
|
2353
3453
|
grid-template-rows: auto 1fr;
|
|
@@ -2446,18 +3546,28 @@ ${safeCode}
|
|
|
2446
3546
|
}
|
|
2447
3547
|
|
|
2448
3548
|
.hai-ai-doc-reader {
|
|
3549
|
+
width: 100%;
|
|
3550
|
+
flex: 1 1 auto;
|
|
2449
3551
|
min-width: 0;
|
|
2450
3552
|
min-height: 0;
|
|
2451
3553
|
position: relative;
|
|
2452
3554
|
z-index: 2;
|
|
2453
3555
|
isolation: isolate;
|
|
3556
|
+
display: flex;
|
|
3557
|
+
flex-direction: column;
|
|
2454
3558
|
}
|
|
2455
3559
|
|
|
2456
3560
|
.hai-ai-doc-scroll {
|
|
2457
3561
|
position: relative;
|
|
3562
|
+
width: 100%;
|
|
3563
|
+
min-width: 0;
|
|
2458
3564
|
height: 100%;
|
|
2459
3565
|
overflow: auto;
|
|
2460
3566
|
padding: 0.5rem 0 1.75rem;
|
|
3567
|
+
display: flex;
|
|
3568
|
+
flex-direction: column;
|
|
3569
|
+
flex: 1 1 auto;
|
|
3570
|
+
min-height: 0;
|
|
2461
3571
|
}
|
|
2462
3572
|
|
|
2463
3573
|
.hai-ai-doc-selection-layer {
|
|
@@ -2466,21 +3576,17 @@ ${safeCode}
|
|
|
2466
3576
|
display: block;
|
|
2467
3577
|
width: fit-content;
|
|
2468
3578
|
max-width: calc(100% - 2rem);
|
|
2469
|
-
transform: translateX(-50%);
|
|
2470
3579
|
transition:
|
|
2471
3580
|
top 0.14s cubic-bezier(0.2, 0.85, 0.24, 1),
|
|
2472
|
-
left 0.14s cubic-bezier(0.2, 0.85, 0.24, 1)
|
|
2473
|
-
transform 0.14s cubic-bezier(0.2, 0.85, 0.24, 1);
|
|
3581
|
+
left 0.14s cubic-bezier(0.2, 0.85, 0.24, 1);
|
|
2474
3582
|
pointer-events: none;
|
|
2475
3583
|
}
|
|
2476
3584
|
|
|
2477
3585
|
.hai-ai-doc-selection-layer[data-alignment='left'] {
|
|
2478
|
-
transform: translateX(0);
|
|
2479
3586
|
justify-items: start;
|
|
2480
3587
|
}
|
|
2481
3588
|
|
|
2482
3589
|
.hai-ai-doc-selection-layer[data-alignment='right'] {
|
|
2483
|
-
transform: translateX(-100%);
|
|
2484
3590
|
justify-items: end;
|
|
2485
3591
|
}
|
|
2486
3592
|
|
|
@@ -2495,6 +3601,55 @@ ${safeCode}
|
|
|
2495
3601
|
max-width: min(100%, calc(100vw - 2rem));
|
|
2496
3602
|
}
|
|
2497
3603
|
|
|
3604
|
+
.hai-ai-doc-link-hover {
|
|
3605
|
+
position: absolute;
|
|
3606
|
+
z-index: 58;
|
|
3607
|
+
display: inline-flex;
|
|
3608
|
+
align-items: center;
|
|
3609
|
+
gap: 0.18rem;
|
|
3610
|
+
padding: 0.22rem;
|
|
3611
|
+
border-radius: 9999px;
|
|
3612
|
+
background: color-mix(in srgb, oklch(var(--b1, 1 0 0)) 94%, white 6%);
|
|
3613
|
+
border: 1px solid color-mix(in srgb, oklch(var(--bc, 0.22 0 0)) 12%, white 88%);
|
|
3614
|
+
box-shadow:
|
|
3615
|
+
0 20px 36px -26px rgb(15 23 42 / 0.26),
|
|
3616
|
+
0 10px 18px -16px rgb(15 23 42 / 0.18);
|
|
3617
|
+
pointer-events: auto;
|
|
3618
|
+
transition:
|
|
3619
|
+
top 0.14s cubic-bezier(0.2, 0.85, 0.24, 1),
|
|
3620
|
+
left 0.14s cubic-bezier(0.2, 0.85, 0.24, 1);
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
.hai-ai-doc-link-hover-btn {
|
|
3624
|
+
width: 2rem;
|
|
3625
|
+
height: 2rem;
|
|
3626
|
+
display: inline-flex;
|
|
3627
|
+
align-items: center;
|
|
3628
|
+
justify-content: center;
|
|
3629
|
+
border: none;
|
|
3630
|
+
border-radius: 9999px;
|
|
3631
|
+
background: transparent;
|
|
3632
|
+
color: oklch(var(--bc) / 0.76);
|
|
3633
|
+
cursor: pointer;
|
|
3634
|
+
transition:
|
|
3635
|
+
background-color 0.14s ease,
|
|
3636
|
+
color 0.14s ease,
|
|
3637
|
+
transform 0.14s ease;
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
.hai-ai-doc-link-hover-btn:hover {
|
|
3641
|
+
background: oklch(var(--bc) / 0.06);
|
|
3642
|
+
color: oklch(var(--bc));
|
|
3643
|
+
transform: translateY(-1px);
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
.hai-ai-doc-link-hover-btn svg {
|
|
3647
|
+
width: 1rem;
|
|
3648
|
+
height: 1rem;
|
|
3649
|
+
fill: none;
|
|
3650
|
+
stroke: currentColor;
|
|
3651
|
+
}
|
|
3652
|
+
|
|
2498
3653
|
.hai-ai-doc-selection-toolbar {
|
|
2499
3654
|
display: flex;
|
|
2500
3655
|
align-items: center;
|
|
@@ -2863,6 +4018,20 @@ ${safeCode}
|
|
|
2863
4018
|
border: 1px solid rgb(15 23 42 / 0.08);
|
|
2864
4019
|
}
|
|
2865
4020
|
|
|
4021
|
+
.hai-ai-doc-color-swatch[data-empty='true'] span {
|
|
4022
|
+
position: relative;
|
|
4023
|
+
border-color: oklch(var(--bc) / 0.18);
|
|
4024
|
+
background:
|
|
4025
|
+
linear-gradient(
|
|
4026
|
+
135deg,
|
|
4027
|
+
transparent calc(50% - 0.75px),
|
|
4028
|
+
oklch(var(--bc) / 0.34) calc(50% - 0.75px),
|
|
4029
|
+
oklch(var(--bc) / 0.34) calc(50% + 0.75px),
|
|
4030
|
+
transparent calc(50% + 0.75px)
|
|
4031
|
+
),
|
|
4032
|
+
color-mix(in srgb, oklch(var(--b1)) 96%, white 4%);
|
|
4033
|
+
}
|
|
4034
|
+
|
|
2866
4035
|
.hai-ai-doc-rewrite-menu-btn {
|
|
2867
4036
|
justify-content: center;
|
|
2868
4037
|
}
|
|
@@ -2875,12 +4044,28 @@ ${safeCode}
|
|
|
2875
4044
|
}
|
|
2876
4045
|
|
|
2877
4046
|
.hai-markdown-document {
|
|
4047
|
+
width: 100%;
|
|
4048
|
+
min-width: 0;
|
|
2878
4049
|
max-width: 52.5rem;
|
|
2879
4050
|
margin: 0 auto;
|
|
2880
4051
|
padding: 1.25rem 2rem 6rem;
|
|
2881
4052
|
outline: none;
|
|
2882
4053
|
}
|
|
2883
4054
|
|
|
4055
|
+
.hai-markdown-document.hai-markdown-code-only {
|
|
4056
|
+
max-width: none !important;
|
|
4057
|
+
width: 100% !important;
|
|
4058
|
+
min-width: 0;
|
|
4059
|
+
margin: 0 !important;
|
|
4060
|
+
height: 100%;
|
|
4061
|
+
min-height: 100%;
|
|
4062
|
+
padding: 0.5rem 0.75rem 0.75rem;
|
|
4063
|
+
display: flex;
|
|
4064
|
+
flex-direction: column;
|
|
4065
|
+
flex: 1 1 auto;
|
|
4066
|
+
box-sizing: border-box;
|
|
4067
|
+
}
|
|
4068
|
+
|
|
2884
4069
|
:global(.hai-markdown-document ::selection) {
|
|
2885
4070
|
background: var(--hai-ai-doc-selection-bg, rgb(59 130 246 / 0.22));
|
|
2886
4071
|
color: var(--hai-ai-doc-selection-fg, inherit);
|
|
@@ -2956,6 +4141,10 @@ ${safeCode}
|
|
|
2956
4141
|
font-weight: 500;
|
|
2957
4142
|
}
|
|
2958
4143
|
|
|
4144
|
+
.hai-markdown-editable :global(a[href]) {
|
|
4145
|
+
cursor: pointer;
|
|
4146
|
+
}
|
|
4147
|
+
|
|
2959
4148
|
.hai-markdown :global(a:hover) {
|
|
2960
4149
|
text-decoration: underline;
|
|
2961
4150
|
}
|
|
@@ -3019,6 +4208,7 @@ ${safeCode}
|
|
|
3019
4208
|
gap: 0.75rem;
|
|
3020
4209
|
padding: 0.65rem 1rem;
|
|
3021
4210
|
min-height: 2.5rem;
|
|
4211
|
+
line-height: 1.2;
|
|
3022
4212
|
background: oklch(var(--n) / 0.94);
|
|
3023
4213
|
border-bottom: 1px solid oklch(var(--nc) / 0.1);
|
|
3024
4214
|
}
|
|
@@ -3027,6 +4217,7 @@ ${safeCode}
|
|
|
3027
4217
|
min-width: 0;
|
|
3028
4218
|
display: flex;
|
|
3029
4219
|
align-items: center;
|
|
4220
|
+
gap: 0.55rem;
|
|
3030
4221
|
}
|
|
3031
4222
|
|
|
3032
4223
|
.hai-markdown :global(.hai-md-code-actions) {
|
|
@@ -3038,12 +4229,54 @@ ${safeCode}
|
|
|
3038
4229
|
.hai-markdown :global(.hai-md-code-lang) {
|
|
3039
4230
|
font-size: 0.75rem;
|
|
3040
4231
|
font-weight: 600;
|
|
4232
|
+
line-height: 1;
|
|
3041
4233
|
text-transform: uppercase;
|
|
3042
4234
|
letter-spacing: 0.05em;
|
|
3043
4235
|
color: oklch(var(--nc) / 0.6);
|
|
3044
4236
|
user-select: none;
|
|
3045
4237
|
}
|
|
3046
4238
|
|
|
4239
|
+
.hai-markdown :global(.hai-md-code-view-switch) {
|
|
4240
|
+
display: inline-flex;
|
|
4241
|
+
align-items: center;
|
|
4242
|
+
gap: 0.16rem;
|
|
4243
|
+
padding: 0.15rem;
|
|
4244
|
+
border-radius: 9999px;
|
|
4245
|
+
border: 1px solid oklch(var(--nc) / 0.16);
|
|
4246
|
+
background: oklch(var(--nc) / 0.08);
|
|
4247
|
+
flex-shrink: 0;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
.hai-markdown :global(.hai-md-code-view-btn) {
|
|
4251
|
+
min-height: 1.7rem;
|
|
4252
|
+
padding: 0 0.72rem;
|
|
4253
|
+
border: none;
|
|
4254
|
+
border-radius: 9999px;
|
|
4255
|
+
background: transparent;
|
|
4256
|
+
color: oklch(var(--nc) / 0.72);
|
|
4257
|
+
font-size: 0.86rem;
|
|
4258
|
+
font-weight: 600;
|
|
4259
|
+
line-height: 1;
|
|
4260
|
+
cursor: pointer;
|
|
4261
|
+
transition:
|
|
4262
|
+
background-color 0.15s ease,
|
|
4263
|
+
color 0.15s ease;
|
|
4264
|
+
}
|
|
4265
|
+
|
|
4266
|
+
.hai-markdown :global(.hai-md-code-view-btn[aria-pressed='true']) {
|
|
4267
|
+
background: oklch(var(--nc) / 0.2);
|
|
4268
|
+
color: oklch(var(--nc));
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
.hai-markdown :global(.hai-md-code-preview-hint) {
|
|
4272
|
+
font-size: 0.75rem;
|
|
4273
|
+
line-height: 1;
|
|
4274
|
+
color: oklch(var(--nc) / 0.48);
|
|
4275
|
+
white-space: nowrap;
|
|
4276
|
+
overflow: hidden;
|
|
4277
|
+
text-overflow: ellipsis;
|
|
4278
|
+
}
|
|
4279
|
+
|
|
3047
4280
|
.hai-markdown :global(.hai-md-code-lang-empty) {
|
|
3048
4281
|
opacity: 0.7;
|
|
3049
4282
|
}
|
|
@@ -3083,8 +4316,21 @@ ${safeCode}
|
|
|
3083
4316
|
ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
|
|
3084
4317
|
'Liberation Mono', monospace;
|
|
3085
4318
|
font-size: 0.875rem;
|
|
3086
|
-
line-height: 1.
|
|
4319
|
+
line-height: 1.45;
|
|
3087
4320
|
tab-size: 2;
|
|
4321
|
+
|
|
4322
|
+
/* Shiki CSS Variables 主题(由 highlight.ts 的 createCssVariablesTheme 引用) */
|
|
4323
|
+
--hai-hl-foreground: oklch(var(--nc));
|
|
4324
|
+
--hai-hl-background: transparent;
|
|
4325
|
+
--hai-hl-token-keyword: oklch(0.7 0.15 280);
|
|
4326
|
+
--hai-hl-token-string: oklch(0.75 0.12 150);
|
|
4327
|
+
--hai-hl-token-string-expression: oklch(0.75 0.12 150);
|
|
4328
|
+
--hai-hl-token-constant: oklch(0.78 0.12 70);
|
|
4329
|
+
--hai-hl-token-comment: oklch(var(--nc) / 0.45);
|
|
4330
|
+
--hai-hl-token-function: oklch(0.72 0.14 220);
|
|
4331
|
+
--hai-hl-token-parameter: oklch(0.72 0.12 35);
|
|
4332
|
+
--hai-hl-token-punctuation: oklch(var(--nc) / 0.7);
|
|
4333
|
+
--hai-hl-token-link: oklch(0.7 0.15 280);
|
|
3088
4334
|
}
|
|
3089
4335
|
|
|
3090
4336
|
.hai-markdown :global(.hai-md-code-preview-slot) {
|
|
@@ -3092,6 +4338,76 @@ ${safeCode}
|
|
|
3092
4338
|
background: oklch(var(--b1));
|
|
3093
4339
|
}
|
|
3094
4340
|
|
|
4341
|
+
.hai-markdown :global(.hai-md-code-block[data-code-view='code'] .hai-md-code-preview-slot) {
|
|
4342
|
+
display: none;
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
.hai-markdown :global(.hai-md-code-block[data-code-view='preview'] pre) {
|
|
4346
|
+
display: none;
|
|
4347
|
+
}
|
|
4348
|
+
|
|
4349
|
+
.hai-markdown :global(.hai-md-code-block[data-code-view='preview'] .hai-md-code-preview-slot) {
|
|
4350
|
+
display: block;
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
.hai-markdown-code-only :global(.hai-md-code-block) {
|
|
4354
|
+
width: 100% !important;
|
|
4355
|
+
max-width: none !important;
|
|
4356
|
+
min-width: 0;
|
|
4357
|
+
margin: 0;
|
|
4358
|
+
height: auto;
|
|
4359
|
+
min-height: 0;
|
|
4360
|
+
flex: 1 1 auto;
|
|
4361
|
+
display: flex !important;
|
|
4362
|
+
align-self: stretch;
|
|
4363
|
+
align-items: stretch;
|
|
4364
|
+
flex-direction: column;
|
|
4365
|
+
overflow: hidden;
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
.hai-markdown-code-only :global(.hai-md-code-header),
|
|
4369
|
+
.hai-markdown-code-only :global(.hai-md-code-block pre),
|
|
4370
|
+
.hai-markdown-code-only :global(.hai-md-code-preview-slot) {
|
|
4371
|
+
width: 100%;
|
|
4372
|
+
min-width: 0;
|
|
4373
|
+
min-height: 0;
|
|
4374
|
+
box-sizing: border-box;
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
/* 代码/预览切换时统一固定 header 高度,避免视觉抖动。 */
|
|
4378
|
+
.hai-markdown-code-only :global(.hai-md-code-header) {
|
|
4379
|
+
flex: 0 0 3.1rem;
|
|
4380
|
+
height: 3.1rem;
|
|
4381
|
+
min-height: 3.1rem;
|
|
4382
|
+
padding: 0 1rem;
|
|
4383
|
+
align-items: center;
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
.hai-markdown-code-only :global(.hai-md-code-block pre),
|
|
4387
|
+
.hai-markdown-code-only :global(.hai-md-code-preview-slot) {
|
|
4388
|
+
flex: 1 1 auto;
|
|
4389
|
+
height: auto;
|
|
4390
|
+
min-height: 0;
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
.hai-markdown-code-only :global(.hai-md-code-block pre) {
|
|
4394
|
+
overflow: auto;
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
.hai-markdown-code-only :global(.hai-md-code-preview-slot) {
|
|
4398
|
+
background: oklch(var(--n));
|
|
4399
|
+
color: oklch(var(--nc));
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
.hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview']) {
|
|
4403
|
+
flex: 1 1 auto;
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
.hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview'] .hai-md-code-preview-slot) {
|
|
4407
|
+
flex: 1 1 auto;
|
|
4408
|
+
display: flex;
|
|
4409
|
+
}
|
|
4410
|
+
|
|
3095
4411
|
.hai-markdown :global(blockquote) {
|
|
3096
4412
|
margin: 1em 0;
|
|
3097
4413
|
padding: 0.65em 1.1em;
|
|
@@ -3209,14 +4525,29 @@ ${safeCode}
|
|
|
3209
4525
|
box-shadow: inset 0 -1px 0 oklch(var(--bc) / 0.1);
|
|
3210
4526
|
}
|
|
3211
4527
|
|
|
3212
|
-
.hai-md-preview-card {
|
|
4528
|
+
:global(.hai-md-preview-card) {
|
|
3213
4529
|
margin: 0;
|
|
3214
4530
|
padding: 1rem;
|
|
3215
4531
|
background: oklch(var(--b1));
|
|
3216
4532
|
color: oklch(var(--bc));
|
|
3217
4533
|
}
|
|
3218
4534
|
|
|
3219
|
-
.hai-md-preview-
|
|
4535
|
+
.hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview'] .hai-md-preview-card) {
|
|
4536
|
+
display: flex;
|
|
4537
|
+
flex-direction: column;
|
|
4538
|
+
flex: 1 1 auto;
|
|
4539
|
+
min-height: 0;
|
|
4540
|
+
padding: 0.95rem 1rem;
|
|
4541
|
+
background: oklch(var(--n));
|
|
4542
|
+
color: oklch(var(--nc));
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
.hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview'] .hai-md-preview-head),
|
|
4546
|
+
.hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview'] .hai-md-preview-desc) {
|
|
4547
|
+
display: none;
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
:global(.hai-md-preview-head) {
|
|
3220
4551
|
font-size: 0.8rem;
|
|
3221
4552
|
font-weight: 700;
|
|
3222
4553
|
letter-spacing: 0.04em;
|
|
@@ -3224,12 +4555,43 @@ ${safeCode}
|
|
|
3224
4555
|
color: oklch(var(--bc) / 0.7);
|
|
3225
4556
|
}
|
|
3226
4557
|
|
|
3227
|
-
.hai-md-preview-desc {
|
|
4558
|
+
:global(.hai-md-preview-desc) {
|
|
3228
4559
|
margin: 0.5rem 0 0;
|
|
3229
4560
|
font-size: 0.875rem;
|
|
3230
4561
|
color: oklch(var(--bc) / 0.7);
|
|
3231
4562
|
}
|
|
3232
4563
|
|
|
4564
|
+
:global(.hai-md-preview-empty) {
|
|
4565
|
+
min-height: 100%;
|
|
4566
|
+
display: flex;
|
|
4567
|
+
flex-direction: column;
|
|
4568
|
+
gap: 0.62rem;
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
:global(.hai-md-preview-empty-title) {
|
|
4572
|
+
margin: 0;
|
|
4573
|
+
font-size: 0.9rem;
|
|
4574
|
+
font-weight: 700;
|
|
4575
|
+
letter-spacing: 0.01em;
|
|
4576
|
+
color: oklch(var(--bc) / 0.85);
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
:global(.hai-md-preview-empty-text) {
|
|
4580
|
+
margin: 0;
|
|
4581
|
+
font-size: 0.84rem;
|
|
4582
|
+
line-height: 1.58;
|
|
4583
|
+
white-space: pre-wrap;
|
|
4584
|
+
color: oklch(var(--bc) / 0.74);
|
|
4585
|
+
}
|
|
4586
|
+
|
|
4587
|
+
.hai-markdown-code-only :global(.hai-md-preview-empty-title) {
|
|
4588
|
+
color: oklch(var(--nc) / 0.88);
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
.hai-markdown-code-only :global(.hai-md-preview-empty-text) {
|
|
4592
|
+
color: oklch(var(--nc) / 0.72);
|
|
4593
|
+
}
|
|
4594
|
+
|
|
3233
4595
|
:global(.hai-md-preview-card pre) {
|
|
3234
4596
|
margin: 0.85rem 0 0;
|
|
3235
4597
|
padding: 0.85rem 1rem;
|
|
@@ -3243,7 +4605,7 @@ ${safeCode}
|
|
|
3243
4605
|
color: oklch(var(--er));
|
|
3244
4606
|
}
|
|
3245
4607
|
|
|
3246
|
-
.hai-md-preview-frame {
|
|
4608
|
+
:global(.hai-md-preview-frame) {
|
|
3247
4609
|
display: block;
|
|
3248
4610
|
width: 100%;
|
|
3249
4611
|
min-height: 280px;
|
|
@@ -3253,10 +4615,19 @@ ${safeCode}
|
|
|
3253
4615
|
background: white;
|
|
3254
4616
|
}
|
|
3255
4617
|
|
|
3256
|
-
.hai-md-preview-
|
|
4618
|
+
.hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview'] .hai-md-preview-frame) {
|
|
4619
|
+
flex: 1 1 auto;
|
|
4620
|
+
height: 100%;
|
|
4621
|
+
min-height: 0;
|
|
4622
|
+
margin-top: 0;
|
|
4623
|
+
border: none;
|
|
4624
|
+
border-radius: 0;
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
:global(.hai-md-preview-loading) {
|
|
3257
4628
|
width: 100%;
|
|
3258
4629
|
height: 4px;
|
|
3259
|
-
margin-top: 0.
|
|
4630
|
+
margin-top: 0.5rem;
|
|
3260
4631
|
border-radius: 9999px;
|
|
3261
4632
|
background: linear-gradient(
|
|
3262
4633
|
90deg,
|
|
@@ -3268,27 +4639,10 @@ ${safeCode}
|
|
|
3268
4639
|
animation: hai-md-loading 1.2s linear infinite;
|
|
3269
4640
|
}
|
|
3270
4641
|
|
|
3271
|
-
.hai-md-preview-rendered :
|
|
4642
|
+
:global(.hai-md-preview-rendered > :first-child) {
|
|
3272
4643
|
margin-top: 0.85rem;
|
|
3273
4644
|
}
|
|
3274
4645
|
|
|
3275
|
-
/* Shiki CSS Variables 主题 — 代码高亮 token 颜色 */
|
|
3276
|
-
.hai-markdown :global(.hai-md-code-block code) {
|
|
3277
|
-
--hai-hl-fg: oklch(var(--nc));
|
|
3278
|
-
--hai-hl-bg: transparent;
|
|
3279
|
-
|
|
3280
|
-
/* token 颜色 */
|
|
3281
|
-
--hai-hl-token-keyword: oklch(0.7 0.15 280);
|
|
3282
|
-
--hai-hl-token-string: oklch(0.75 0.12 150);
|
|
3283
|
-
--hai-hl-token-string-expression: oklch(0.75 0.12 150);
|
|
3284
|
-
--hai-hl-token-constant: oklch(0.78 0.12 70);
|
|
3285
|
-
--hai-hl-token-comment: oklch(var(--nc) / 0.45);
|
|
3286
|
-
--hai-hl-token-function: oklch(0.72 0.14 220);
|
|
3287
|
-
--hai-hl-token-parameter: oklch(0.72 0.12 35);
|
|
3288
|
-
--hai-hl-token-punctuation: oklch(var(--nc) / 0.7);
|
|
3289
|
-
--hai-hl-token-link: oklch(0.7 0.15 280);
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
4646
|
@keyframes hai-md-loading {
|
|
3293
4647
|
from {
|
|
3294
4648
|
background-position: 220px 0;
|
|
@@ -3299,16 +4653,31 @@ ${safeCode}
|
|
|
3299
4653
|
}
|
|
3300
4654
|
}
|
|
3301
4655
|
|
|
3302
|
-
@
|
|
3303
|
-
.hai-ai-doc-layout
|
|
3304
|
-
|
|
3305
|
-
|
|
4656
|
+
@container ai-doc-editor (max-width: 800px) {
|
|
4657
|
+
.hai-ai-doc-layout {
|
|
4658
|
+
display: flex;
|
|
4659
|
+
min-height: 0;
|
|
3306
4660
|
}
|
|
3307
4661
|
|
|
3308
4662
|
.hai-ai-doc-outline {
|
|
3309
|
-
|
|
4663
|
+
position: absolute;
|
|
4664
|
+
top: 0.65rem;
|
|
4665
|
+
left: 0.65rem;
|
|
4666
|
+
bottom: 0.85rem;
|
|
4667
|
+
width: min(220px, calc(100% - 1.3rem));
|
|
4668
|
+
max-width: 220px;
|
|
4669
|
+
z-index: 14;
|
|
4670
|
+
padding: 0.5rem 0.625rem 0.9rem 0.75rem;
|
|
4671
|
+
border: 1px solid oklch(var(--bc) / 0.12);
|
|
4672
|
+
border-radius: 0.95rem;
|
|
4673
|
+
background:
|
|
4674
|
+
linear-gradient(180deg, oklch(var(--b2) / 0.84), oklch(var(--b1) / 0.92));
|
|
4675
|
+
box-shadow: 0 20px 40px -28px oklch(var(--bc) / 0.35);
|
|
4676
|
+
backdrop-filter: blur(8px);
|
|
3310
4677
|
}
|
|
4678
|
+
}
|
|
3311
4679
|
|
|
4680
|
+
@media (max-width: 960px) {
|
|
3312
4681
|
.hai-ai-doc-topbar {
|
|
3313
4682
|
padding-left: 1rem;
|
|
3314
4683
|
padding-right: 1rem;
|