@h-ai/ui 0.1.0-alpha.10 → 0.1.0-alpha.12

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.
@@ -14,11 +14,14 @@
14
14
  import type {
15
15
  AiDocumentEditorProps,
16
16
  MarkdownBlockFormatKind,
17
+ MarkdownBlockStyleKind,
17
18
  MarkdownCodeBlockItem,
18
19
  MarkdownCodeRunRequest,
19
20
  MarkdownCodeRunResult,
21
+ MarkdownColorFormatRequest,
20
22
  MarkdownInlineFormatKind,
21
23
  MarkdownRewriteAction,
24
+ MarkdownTextAlignKind,
22
25
  } from './document-types.js'
23
26
  import { uiM } from '../../../messages.js'
24
27
  import { cn } from '../../../utils.js'
@@ -47,10 +50,132 @@
47
50
  error?: string
48
51
  }
49
52
 
53
+ interface SelectionFormatState {
54
+ /** 当前选区所在段落样式,用于标题/正文下拉默认值。 */
55
+ blockFormat: MarkdownBlockStyleKind
56
+ /** 当前选区所在段落的对齐方式。 */
57
+ alignment: MarkdownTextAlignKind
58
+ /** 当前选区是否命中加粗样式。 */
59
+ bold: boolean
60
+ /** 当前选区是否命中删除线样式。 */
61
+ strike: boolean
62
+ /** 当前选区是否命中斜体样式。 */
63
+ italic: boolean
64
+ /** 当前选区是否命中下划线样式。 */
65
+ underline: boolean
66
+ /** 当前选区是否命中 `<mark>` 或自定义底色高亮。 */
67
+ highlight: boolean
68
+ /** 当前选区是否落在行内代码内。 */
69
+ code: boolean
70
+ /** 当前选区命中的链接地址;为空表示无链接。 */
71
+ linkHref: string
72
+ /** 当前选区前景色;为空表示使用默认文字颜色。 */
73
+ textColor: string | null
74
+ /** 当前选区背景色;为空表示使用默认背景。 */
75
+ backgroundColor: string | null
76
+ }
77
+
78
+ interface PreviewSelectionState {
79
+ /** 浏览器当前仍然有效的正文选区。 */
80
+ selection: Selection
81
+ /** 当前选区对应的 DOM Range,用于计算浮层定位。 */
82
+ range: Range
83
+ /** 归一化后的选中文本。 */
84
+ text: string
85
+ }
86
+
87
+ type SelectionMenuKind = 'rewrite' | 'block' | 'align' | 'link' | 'color'
88
+
89
+ const BLOCK_FORMAT_OPTIONS: Array<{
90
+ value: MarkdownBlockStyleKind
91
+ labelKey:
92
+ | 'markdown_format_paragraph'
93
+ | 'markdown_format_heading_1'
94
+ | 'markdown_format_heading_2'
95
+ | 'markdown_format_heading_3'
96
+ | 'markdown_format_heading_4'
97
+ shortLabel: string
98
+ }> = [
99
+ {
100
+ value: 'paragraph',
101
+ labelKey: 'markdown_format_paragraph',
102
+ shortLabel: 'T',
103
+ },
104
+ {
105
+ value: 'heading1',
106
+ labelKey: 'markdown_format_heading_1',
107
+ shortLabel: 'H1',
108
+ },
109
+ {
110
+ value: 'heading2',
111
+ labelKey: 'markdown_format_heading_2',
112
+ shortLabel: 'H2',
113
+ },
114
+ {
115
+ value: 'heading3',
116
+ labelKey: 'markdown_format_heading_3',
117
+ shortLabel: 'H3',
118
+ },
119
+ {
120
+ value: 'heading4',
121
+ labelKey: 'markdown_format_heading_4',
122
+ shortLabel: 'H4',
123
+ },
124
+ ]
125
+
126
+ const ALIGN_OPTIONS: Array<{
127
+ value: MarkdownTextAlignKind
128
+ labelKey:
129
+ | 'markdown_align_left'
130
+ | 'markdown_align_center'
131
+ | 'markdown_align_right'
132
+ | 'markdown_align_justify'
133
+ }> = [
134
+ { value: 'left', labelKey: 'markdown_align_left' },
135
+ { value: 'center', labelKey: 'markdown_align_center' },
136
+ { value: 'right', labelKey: 'markdown_align_right' },
137
+ { value: 'justify', labelKey: 'markdown_align_justify' },
138
+ ]
139
+
140
+ const TEXT_COLOR_PRESETS = [
141
+ '#0f172a',
142
+ '#475569',
143
+ '#b91c1c',
144
+ '#ea580c',
145
+ '#ca8a04',
146
+ '#15803d',
147
+ '#2563eb',
148
+ '#7c3aed',
149
+ ] as const
150
+
151
+ const BACKGROUND_COLOR_PRESETS = [
152
+ '#fee2e2',
153
+ '#ffedd5',
154
+ '#fef3c7',
155
+ '#dcfce7',
156
+ '#dbeafe',
157
+ '#ede9fe',
158
+ '#f3f4f6',
159
+ '#e2e8f0',
160
+ ] as const
161
+
162
+ const DEFAULT_SELECTION_FORMAT_STATE: SelectionFormatState = {
163
+ blockFormat: 'paragraph',
164
+ alignment: 'left',
165
+ bold: false,
166
+ strike: false,
167
+ italic: false,
168
+ underline: false,
169
+ highlight: false,
170
+ code: false,
171
+ linkHref: '',
172
+ textColor: null,
173
+ backgroundColor: null,
174
+ }
175
+
50
176
  // 复制前后的图标以内联 SVG 缓存,避免每次点击都重新拼接按钮内容。
51
177
  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>`
52
178
  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>`
53
- const HTML_SNIPPET_REGEX = /<!doctype html>|<html[\s>]|<body[\s>]|<div[\s>]|<main[\s>]/i
54
179
 
55
180
  let {
56
181
  // 暴露给外层的滚动容器引用,用于同步滚动或定位选区工具条。
@@ -71,6 +196,10 @@
71
196
  showCopyButton = true,
72
197
  // 是否显示代码块运行按钮与预览占位。
73
198
  showRunButton = false,
199
+ // 是否在代码块头部显示“代码/预览”切换。
200
+ showCodePreviewToggle = false,
201
+ // 代码预览切换模式下展示在语言标签旁的提示文案。
202
+ codePreviewHint = '',
74
203
  // 是否启用语法高亮。
75
204
  enableHighlight = true,
76
205
  // 是否把换行渲染为 <br>。
@@ -123,8 +252,16 @@
123
252
  oncopydocument,
124
253
  // 块级格式动作回调。
125
254
  onapplyblockformat,
255
+ // 细粒度块样式动作回调。
256
+ onapplyblockstyle,
126
257
  // 行内格式动作回调。
127
258
  onapplyinlineformat,
259
+ // 对齐动作回调。
260
+ onapplyalignment,
261
+ // 链接设置动作回调。
262
+ onapplylink,
263
+ // 颜色设置动作回调。
264
+ onapplycolor,
128
265
  // 选区复制回调。
129
266
  oncopyselection,
130
267
  // 选区注释回调。
@@ -141,10 +278,10 @@
141
278
  let selectedText = $state('')
142
279
  // 选区工具条显示开关。
143
280
  let selectionToolbarVisible = $state(false)
144
- // 改写菜单显示开关。
145
- let rewriteMenuOpen = $state(false)
281
+ // 选区工具条当前展开的子菜单,统一管理避免多个面板互相覆盖。
282
+ let activeSelectionMenu = $state<SelectionMenuKind | null>(null)
146
283
  // 文档头部标题只有在正文首个标题滚出可视区后才显示,避免双标题并排出现。
147
- let showPinnedTitle = $state(Boolean(title))
284
+ let showPinnedTitle = $state(false)
148
285
  // 选区工具条在滚动容器中的定位坐标。
149
286
  let toolbarPosition = $state<SelectionToolbarPosition>({
150
287
  top: 0,
@@ -152,23 +289,52 @@
152
289
  placement: 'top',
153
290
  alignment: 'center',
154
291
  })
292
+ // 选区工具条自身 DOM,用于根据真实尺寸修正贴边位置。
293
+ let selectionToolbarEl = $state<HTMLDivElement | null>(null)
294
+ // 当前展开菜单对应的触发按钮,用来把面板锚定到具体按钮下方/上方。
295
+ let activeSelectionMenuTrigger = $state<HTMLElement | null>(null)
296
+ // 菜单面板相对工具条的水平锚点。
297
+ let selectionMenuLeft = $state(0)
298
+ // 菜单面板在锚点处的对齐方式,避免宽面板越过正文边界。
299
+ let selectionMenuAlignment = $state<'left' | 'center' | 'right'>('center')
155
300
  // documentCopied 只负责顶部复制按钮的瞬时反馈,不和正文内容状态混用。
156
301
  let documentCopied = $state(false)
157
302
  // copyFeedbackTimer 用来保证连续点击复制时,成功态能按最后一次操作重新计时。
158
- let copyFeedbackTimer: ReturnType<typeof window.setTimeout> | undefined = $state()
303
+ let copyFeedbackTimer: number | undefined = $state()
159
304
  // 每个代码块的运行状态和预览结果,key 为 codeBlockId。
160
305
  let codePreviews = $state<Record<string, CodePreviewState>>({})
306
+ // selectionFormatState 跟随真实 DOM 选区,负责驱动按钮按下态与下拉默认值。
307
+ let selectionFormatState = $state<SelectionFormatState>({
308
+ ...DEFAULT_SELECTION_FORMAT_STATE,
309
+ })
310
+ // linkDraft 只在链接弹层里暂存输入值,避免每次打开都丢掉正在编辑的链接。
311
+ let linkDraft = $state('')
312
+ // 真实尺寸修正只需要排队一帧,避免反复触发 handleSelectionChange 形成抖动。
313
+ let selectionToolbarMeasurePending = false
314
+ // 选区刷新统一合并到同一帧,避免 selectionchange / mouseup / blur 连续触发时抖动。
315
+ let selectionRefreshFrame: number | undefined
316
+ let selectionRefreshNeedsRemeasure = false
317
+ // 浏览器在滚动到底部或切换焦点时会短暂给出空选区,这里延迟一拍再关闭工具条。
318
+ let selectionToolbarCloseTimer: number | undefined
161
319
 
162
320
  // code 类型产物通常只有裸代码文本,这里统一包成 fenced block 进入同一条渲染链路。
163
321
  const documentContent = $derived(
164
322
  resolveDocumentMarkdownContent(content, sourceKind, codeLanguage),
165
323
  )
166
324
  // 渲染结果同时提供 HTML、目录和代码块元数据,供顶部目录与代码预览共用。
325
+ const codePreviewToggleEnabled = $derived(
326
+ showCodePreviewToggle && sourceKind === 'code',
327
+ )
328
+ const normalizedCodePreviewHint = $derived(codePreviewHint.trim())
167
329
  const renderResult = $derived(
168
330
  renderMarkdownDocument(documentContent, {
169
331
  enableHighlight,
170
332
  showCopyButton,
171
333
  showRunButton,
334
+ showCodePreviewToggle: codePreviewToggleEnabled,
335
+ codeViewCodeLabel: uiM('markdown_format_code'),
336
+ codeViewPreviewLabel: uiM('file_list_preview'),
337
+ codePreviewHint: normalizedCodePreviewHint,
172
338
  breaks,
173
339
  }),
174
340
  )
@@ -182,21 +348,44 @@
182
348
  const resolvedRewriteActions = $derived(
183
349
  resolveRewriteActions(rewriteActions, onrewrite),
184
350
  )
351
+ const directRewriteAction = $derived(
352
+ resolvedRewriteActions.length === 1 ? resolvedRewriteActions[0] : null,
353
+ )
354
+ // 新版工具条支持 paragraph / heading1-4,优先走细粒度回调,避免把旧接口误当成新接口调用。
355
+ const richBlockFormattingEnabled = $derived(Boolean(onapplyblockstyle))
356
+ // 旧版 heading / bullet 回调仍然保留,用于兼容外部还没迁移的调用方。
357
+ const legacyBlockFormattingEnabled = $derived(
358
+ Boolean(onapplyblockformat) && !onapplyblockstyle,
359
+ )
360
+ // 这里统一判断是否存在任意块级格式能力,方便控制分隔线和浮层显隐。
361
+ const anyBlockFormattingEnabled = $derived(
362
+ richBlockFormattingEnabled || legacyBlockFormattingEnabled,
363
+ )
185
364
  // selectionToolsEnabled 统一判断选区工具条是否值得出现,避免正文只读展示时还露出空浮层。
186
365
  const selectionToolsEnabled = $derived(
187
366
  [
188
367
  onrewrite,
189
- onapplyblockformat,
368
+ anyBlockFormattingEnabled,
190
369
  onapplyinlineformat,
370
+ onapplyalignment,
371
+ onapplylink,
372
+ onapplycolor,
191
373
  oncopyselection,
192
374
  onannotation,
193
375
  ].some(Boolean),
194
376
  )
377
+ // 当前段落样式的展示文案,直接来自 `selectionFormatState`,避免按钮标题和下拉选中值不同步。
378
+ const activeBlockFormatOption = $derived(
379
+ BLOCK_FORMAT_OPTIONS.find(
380
+ option => option.value === selectionFormatState.blockFormat,
381
+ ) ?? BLOCK_FORMAT_OPTIONS[0],
382
+ )
195
383
  // readerDocumentClass 只负责正文文章区域,不和外层容器类名混用。
196
384
  const readerDocumentClass = $derived(
197
385
  cn(
198
386
  'hai-markdown',
199
387
  'hai-markdown-document',
388
+ sourceKind === 'code' ? 'hai-markdown-code-only' : '',
200
389
  editable ? 'hai-markdown-editable' : '',
201
390
  ),
202
391
  )
@@ -242,6 +431,7 @@
242
431
 
243
432
  $effect(() => {
244
433
  void documentContent
434
+ codePreviews = {}
245
435
  closeSelectionToolbar()
246
436
  })
247
437
 
@@ -259,6 +449,33 @@
259
449
  if (copyFeedbackTimer) {
260
450
  window.clearTimeout(copyFeedbackTimer)
261
451
  }
452
+
453
+ if (selectionRefreshFrame) {
454
+ window.cancelAnimationFrame(selectionRefreshFrame)
455
+ }
456
+
457
+ if (selectionToolbarCloseTimer) {
458
+ window.clearTimeout(selectionToolbarCloseTimer)
459
+ }
460
+ }
461
+ })
462
+
463
+ $effect(() => {
464
+ if (typeof document === 'undefined' || !previewHost || !selectionToolsEnabled) {
465
+ return
466
+ }
467
+
468
+ const handleDocumentSelectionChange = (): void => {
469
+ queueSelectionRefresh()
470
+ }
471
+
472
+ document.addEventListener('selectionchange', handleDocumentSelectionChange)
473
+
474
+ return () => {
475
+ document.removeEventListener(
476
+ 'selectionchange',
477
+ handleDocumentSelectionChange,
478
+ )
262
479
  }
263
480
  })
264
481
 
@@ -282,6 +499,8 @@
282
499
  ]
283
500
  }
284
501
 
502
+ const HTML_DOCUMENT_PATTERN = /<!doctype html>|<html[\s>]|<body[\s>]|<div[\s>]|<main[\s>]/i
503
+
285
504
  async function copyRawContent(): Promise<boolean> {
286
505
  try {
287
506
  await navigator.clipboard.writeText(content)
@@ -348,9 +567,7 @@
348
567
  }
349
568
 
350
569
  function looksLikeHtml(code: string): boolean {
351
- return HTML_SNIPPET_REGEX.test(
352
- code,
353
- )
570
+ return HTML_DOCUMENT_PATTERN.test(code)
354
571
  }
355
572
 
356
573
  function buildJavaScriptPreview(code: string): string {
@@ -467,6 +684,38 @@ ${safeCode}
467
684
  return undefined
468
685
  }
469
686
 
687
+ function formatPreviewLanguageLabel(language: string | undefined): string | null {
688
+ const normalized = language?.trim().toLocaleLowerCase()
689
+ if (!normalized) {
690
+ return null
691
+ }
692
+
693
+ if (
694
+ normalized === 'javascript'
695
+ || normalized === 'js'
696
+ || normalized === 'mjs'
697
+ ) {
698
+ return 'JavaScript'
699
+ }
700
+
701
+ if (normalized === 'markdown' || normalized === 'md') {
702
+ return 'Markdown'
703
+ }
704
+
705
+ return normalized.toUpperCase()
706
+ }
707
+
708
+ function resolvePreviewUnavailableMessage(language: string | undefined): string {
709
+ const languageLabel = formatPreviewLanguageLabel(language)
710
+ if (!languageLabel) {
711
+ return uiM('markdown_run_unavailable')
712
+ }
713
+
714
+ return uiM('markdown_run_unavailable_language', {
715
+ language: languageLabel,
716
+ })
717
+ }
718
+
470
719
  async function runCodeBlock(blockId: string): Promise<void> {
471
720
  const codeBlock = lookupCodeBlock(blockId)
472
721
  if (!codeBlock) {
@@ -497,7 +746,7 @@ ${safeCode}
497
746
  ...codePreviews,
498
747
  [blockId]: {
499
748
  status: 'error',
500
- error: uiM('markdown_run_unavailable'),
749
+ error: resolvePreviewUnavailableMessage(request.language),
501
750
  },
502
751
  }
503
752
  return
@@ -523,6 +772,20 @@ ${safeCode}
523
772
  }
524
773
  }
525
774
 
775
+ function setCodeBlockView(
776
+ codeBlock: HTMLElement,
777
+ view: 'code' | 'preview',
778
+ ): void {
779
+ codeBlock.dataset.codeView = view
780
+ const toggleButtons = codeBlock.querySelectorAll<HTMLButtonElement>(
781
+ '[data-code-view-toggle]',
782
+ )
783
+ for (const button of toggleButtons) {
784
+ const isActive = button.dataset.codeView === view
785
+ button.setAttribute('aria-pressed', isActive ? 'true' : 'false')
786
+ }
787
+ }
788
+
526
789
  async function handleClick(event: MouseEvent): Promise<void> {
527
790
  const target = event.target as HTMLElement
528
791
  const copyButton = target.closest(
@@ -533,6 +796,26 @@ ${safeCode}
533
796
  return
534
797
  }
535
798
 
799
+ const viewButton = target.closest(
800
+ '[data-code-view-toggle]',
801
+ ) as HTMLButtonElement | null
802
+ const viewCodeBlock = viewButton?.closest<HTMLElement>('.hai-md-code-block')
803
+ const viewCodeBlockId = viewButton?.dataset.codeBlockId
804
+ const nextView = viewButton?.dataset.codeView === 'preview'
805
+ ? 'preview'
806
+ : 'code'
807
+ if (viewButton && viewCodeBlock) {
808
+ setCodeBlockView(viewCodeBlock, nextView)
809
+ if (
810
+ nextView === 'preview'
811
+ && viewCodeBlockId
812
+ && codePreviews[viewCodeBlockId]?.status !== 'ready'
813
+ ) {
814
+ await runCodeBlock(viewCodeBlockId)
815
+ }
816
+ return
817
+ }
818
+
536
819
  const runButton = target.closest(
537
820
  '[data-run-code]',
538
821
  ) as HTMLButtonElement | null
@@ -574,12 +857,12 @@ ${safeCode}
574
857
  }
575
858
 
576
859
  if (preview.status === 'running') {
577
- host.innerHTML = `<div class="hai-md-preview-card"><div class="hai-md-preview-head">${escapePreviewText(uiM('markdown_running'))}</div><div class="hai-md-preview-loading"></div></div>`
860
+ 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>`
578
861
  return
579
862
  }
580
863
 
581
864
  if (preview.status === 'error') {
582
- host.innerHTML = `<div class="hai-md-preview-card hai-md-preview-error"><div class="hai-md-preview-head">${escapePreviewText(uiM('markdown_run_failed'))}</div><pre>${escapePreviewText(preview.error ?? uiM('markdown_run_failed'))}</pre></div>`
865
+ 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>`
583
866
  return
584
867
  }
585
868
 
@@ -605,7 +888,6 @@ ${safeCode}
605
888
  const markdownHtml = parseMarkdown(result.content, {
606
889
  enableHighlight,
607
890
  showCopyButton: false,
608
- showRunButton: false,
609
891
  breaks,
610
892
  })
611
893
 
@@ -629,10 +911,323 @@ ${safeCode}
629
911
  return escapePreviewText(value).replace(/\n/g, '&#10;')
630
912
  }
631
913
 
914
+ function getSelectionAnchorElement(selection: Selection): HTMLElement | null {
915
+ const anchorNode = selection.anchorNode
916
+ if (!anchorNode) {
917
+ return null
918
+ }
919
+
920
+ return anchorNode instanceof HTMLElement
921
+ ? anchorNode
922
+ : anchorNode.parentElement
923
+ }
924
+
925
+ function getClosestBlockElement(element: HTMLElement | null): HTMLElement | null {
926
+ if (!element || !previewHost) {
927
+ return null
928
+ }
929
+
930
+ const block = element.closest('p, h1, h2, h3, h4, h5, h6, li')
931
+ return block instanceof HTMLElement && previewHost.contains(block)
932
+ ? block
933
+ : null
934
+ }
935
+
936
+ function readComputedInlineState(element: HTMLElement | null): Pick<
937
+ SelectionFormatState,
938
+ 'bold' | 'italic' | 'strike' | 'underline'
939
+ > {
940
+ if (!element || typeof window === 'undefined') {
941
+ return {
942
+ bold: false,
943
+ italic: false,
944
+ strike: false,
945
+ underline: false,
946
+ }
947
+ }
948
+
949
+ const style = window.getComputedStyle(element)
950
+ const fontWeight = Number.parseInt(style.fontWeight, 10)
951
+ const decoration = `${style.textDecorationLine} ${style.textDecoration}`
952
+ .toLowerCase()
953
+
954
+ return {
955
+ bold: Number.isNaN(fontWeight)
956
+ ? ['bold', 'bolder'].includes(style.fontWeight.toLowerCase())
957
+ : fontWeight >= 600,
958
+ italic:
959
+ style.fontStyle === 'italic' || style.fontStyle.startsWith('oblique'),
960
+ strike: decoration.includes('line-through'),
961
+ underline: decoration.includes('underline'),
962
+ }
963
+ }
964
+
965
+ function readBlockFormat(element: HTMLElement | null): MarkdownBlockStyleKind {
966
+ const tagName = getClosestBlockElement(element)?.tagName.toLowerCase()
967
+
968
+ if (tagName === 'h1') {
969
+ return 'heading1'
970
+ }
971
+ if (tagName === 'h2') {
972
+ return 'heading2'
973
+ }
974
+ if (tagName === 'h3') {
975
+ return 'heading3'
976
+ }
977
+ if (tagName === 'h4') {
978
+ return 'heading4'
979
+ }
980
+
981
+ return 'paragraph'
982
+ }
983
+
984
+ function normalizeTextAlign(value: string | null | undefined): MarkdownTextAlignKind | null {
985
+ const normalized = value?.trim().toLowerCase()
986
+ if (!normalized) {
987
+ return null
988
+ }
989
+
990
+ if (normalized === 'start') {
991
+ return 'left'
992
+ }
993
+ if (normalized === 'end') {
994
+ return 'right'
995
+ }
996
+
997
+ return normalized === 'center'
998
+ || normalized === 'right'
999
+ || normalized === 'justify'
1000
+ || normalized === 'left'
1001
+ ? normalized
1002
+ : null
1003
+ }
1004
+
1005
+ function readAlignment(element: HTMLElement | null): MarkdownTextAlignKind {
1006
+ const block = getClosestBlockElement(element)
1007
+ if (!block || typeof window === 'undefined') {
1008
+ return 'left'
1009
+ }
1010
+
1011
+ // 对齐样式可能挂在外层 `<hai-align>` 容器上,单读 block 的 inline style 会丢失真实状态。
1012
+ const alignHost = block.closest<HTMLElement>('.hai-md-align-block') || block
1013
+ const computedAlign = normalizeTextAlign(
1014
+ window.getComputedStyle(alignHost).textAlign,
1015
+ )
1016
+ const attributeAlignCandidate
1017
+ = alignHost.dataset.haiAlign
1018
+ || alignHost.getAttribute('align')
1019
+ || block.getAttribute('align')
1020
+ const attributeAlign = normalizeTextAlign(
1021
+ attributeAlignCandidate,
1022
+ )
1023
+
1024
+ return computedAlign || attributeAlign || 'left'
1025
+ }
1026
+
1027
+ function readSelectionFormatState(selection: Selection): SelectionFormatState {
1028
+ const anchorElement = getSelectionAnchorElement(selection)
1029
+ const colorHost = anchorElement?.closest<HTMLElement>('[data-hai-color], [data-hai-bg]')
1030
+ const linkHost = anchorElement?.closest<HTMLAnchorElement>('a[href]')
1031
+ const codeHost = anchorElement?.closest<HTMLElement>('code')
1032
+ const highlightHost = anchorElement?.closest<HTMLElement>('mark')
1033
+ const inlineState = readComputedInlineState(anchorElement)
1034
+ const hasHighlight = Boolean((highlightHost && previewHost?.contains(highlightHost)) || colorHost?.dataset.haiBg?.trim())
1035
+ const textColor = colorHost?.dataset.haiColor?.trim()
1036
+ || anchorElement?.closest<HTMLElement>('[data-hai-color]')?.dataset.haiColor
1037
+ || null
1038
+ const backgroundColor = colorHost?.dataset.haiBg?.trim()
1039
+ || anchorElement?.closest<HTMLElement>('[data-hai-bg]')?.dataset.haiBg
1040
+ || null
1041
+
1042
+ return {
1043
+ blockFormat: readBlockFormat(anchorElement),
1044
+ alignment: readAlignment(anchorElement),
1045
+ bold: inlineState.bold,
1046
+ strike: inlineState.strike,
1047
+ italic: inlineState.italic,
1048
+ underline: inlineState.underline,
1049
+ highlight: hasHighlight,
1050
+ code: Boolean(codeHost && previewHost?.contains(codeHost)),
1051
+ linkHref:
1052
+ linkHost && previewHost?.contains(linkHost)
1053
+ ? (linkHost.getAttribute('href') ?? '')
1054
+ : '',
1055
+ textColor,
1056
+ backgroundColor,
1057
+ }
1058
+ }
1059
+
1060
+ function readPreviewSelectionState(): PreviewSelectionState | null {
1061
+ if (typeof window === 'undefined' || !previewHost) {
1062
+ return null
1063
+ }
1064
+
1065
+ const selection = window.getSelection()
1066
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
1067
+ return null
1068
+ }
1069
+
1070
+ const anchorNode = selection.anchorNode
1071
+ const focusNode = selection.focusNode
1072
+ if (
1073
+ !anchorNode
1074
+ || !focusNode
1075
+ || !previewHost.contains(anchorNode)
1076
+ || !previewHost.contains(focusNode)
1077
+ ) {
1078
+ return null
1079
+ }
1080
+
1081
+ const text = selection.toString().trim()
1082
+ if (!text) {
1083
+ return null
1084
+ }
1085
+
1086
+ return {
1087
+ selection,
1088
+ range: selection.getRangeAt(0),
1089
+ text,
1090
+ }
1091
+ }
1092
+
1093
+ function clearPendingSelectionToolbarClose(): void {
1094
+ if (typeof window === 'undefined' || !selectionToolbarCloseTimer) {
1095
+ return
1096
+ }
1097
+
1098
+ window.clearTimeout(selectionToolbarCloseTimer)
1099
+ selectionToolbarCloseTimer = undefined
1100
+ }
1101
+
1102
+ function scheduleSelectionToolbarClose(): void {
1103
+ if (typeof window === 'undefined') {
1104
+ closeSelectionToolbar()
1105
+ return
1106
+ }
1107
+
1108
+ if (!selectionToolbarVisible) {
1109
+ closeSelectionToolbar()
1110
+ return
1111
+ }
1112
+
1113
+ clearPendingSelectionToolbarClose()
1114
+ selectionToolbarCloseTimer = window.setTimeout(() => {
1115
+ selectionToolbarCloseTimer = undefined
1116
+ if (!readPreviewSelectionState()) {
1117
+ closeSelectionToolbar()
1118
+ }
1119
+ }, 120)
1120
+ }
1121
+
1122
+ function queueSelectionRefresh(remeasured = false): void {
1123
+ if (typeof window === 'undefined') {
1124
+ return
1125
+ }
1126
+
1127
+ selectionRefreshNeedsRemeasure ||= remeasured
1128
+ if (selectionRefreshFrame) {
1129
+ return
1130
+ }
1131
+
1132
+ selectionRefreshFrame = window.requestAnimationFrame(() => {
1133
+ const nextRemeasured = selectionRefreshNeedsRemeasure
1134
+ selectionRefreshFrame = undefined
1135
+ selectionRefreshNeedsRemeasure = false
1136
+ handleSelectionChange(nextRemeasured)
1137
+ })
1138
+ }
1139
+
1140
+ function estimateSelectionMenuWidth(menu: SelectionMenuKind): number {
1141
+ if (menu === 'color' || menu === 'link') {
1142
+ return 256
1143
+ }
1144
+
1145
+ if (menu === 'rewrite') {
1146
+ return 320
1147
+ }
1148
+
1149
+ return 288
1150
+ }
1151
+
1152
+ function updateSelectionMenuAnchor(menu: SelectionMenuKind): void {
1153
+ if (!selectionToolbarEl || !activeSelectionMenuTrigger) {
1154
+ selectionMenuLeft = 0
1155
+ selectionMenuAlignment = 'center'
1156
+ return
1157
+ }
1158
+
1159
+ const toolbarWidth = Math.max(
1160
+ selectionToolbarEl.offsetWidth,
1161
+ selectionToolbarEl.clientWidth,
1162
+ )
1163
+ const triggerCenter
1164
+ = activeSelectionMenuTrigger.offsetLeft
1165
+ + activeSelectionMenuTrigger.offsetWidth / 2
1166
+ const estimatedHalfWidth = Math.min(
1167
+ estimateSelectionMenuWidth(menu) / 2,
1168
+ Math.max(112, (toolbarWidth - 16) / 2),
1169
+ )
1170
+ const minCenter = 8 + estimatedHalfWidth
1171
+ const maxCenter = Math.max(minCenter, toolbarWidth - 8 - estimatedHalfWidth)
1172
+
1173
+ if (triggerCenter <= minCenter) {
1174
+ selectionMenuLeft = 0
1175
+ selectionMenuAlignment = 'left'
1176
+ return
1177
+ }
1178
+
1179
+ if (triggerCenter >= maxCenter) {
1180
+ selectionMenuLeft = toolbarWidth
1181
+ selectionMenuAlignment = 'right'
1182
+ return
1183
+ }
1184
+
1185
+ selectionMenuLeft = triggerCenter
1186
+ selectionMenuAlignment = 'center'
1187
+ }
1188
+
1189
+ function scheduleSelectionToolbarMeasurement(): void {
1190
+ if (
1191
+ typeof window === 'undefined'
1192
+ || selectionToolbarMeasurePending
1193
+ || !selectionToolbarVisible
1194
+ ) {
1195
+ return
1196
+ }
1197
+
1198
+ selectionToolbarMeasurePending = true
1199
+ window.requestAnimationFrame(() => {
1200
+ selectionToolbarMeasurePending = false
1201
+ if (selectionToolbarVisible) {
1202
+ handleSelectionChange(true)
1203
+ }
1204
+ })
1205
+ }
1206
+
1207
+ function toggleSelectionMenu(menu: SelectionMenuKind, event?: MouseEvent): void {
1208
+ const opening = activeSelectionMenu !== menu
1209
+ activeSelectionMenu = opening ? menu : null
1210
+ activeSelectionMenuTrigger = opening && event?.currentTarget instanceof HTMLElement
1211
+ ? event.currentTarget
1212
+ : null
1213
+ if (opening) {
1214
+ updateSelectionMenuAnchor(menu)
1215
+ }
1216
+ if (opening && menu === 'link') {
1217
+ linkDraft = selectionFormatState.linkHref || 'https://'
1218
+ }
1219
+ }
1220
+
632
1221
  function closeSelectionToolbar(): void {
1222
+ clearPendingSelectionToolbarClose()
633
1223
  selectionToolbarVisible = false
634
- rewriteMenuOpen = false
1224
+ activeSelectionMenu = null
1225
+ activeSelectionMenuTrigger = null
1226
+ selectionMenuLeft = 0
1227
+ selectionMenuAlignment = 'center'
635
1228
  selectedText = ''
1229
+ linkDraft = ''
1230
+ selectionFormatState = { ...DEFAULT_SELECTION_FORMAT_STATE }
636
1231
  }
637
1232
 
638
1233
  /**
@@ -683,9 +1278,8 @@ ${safeCode}
683
1278
  return
684
1279
  }
685
1280
 
686
- const firstHeading = previewHost.querySelector<HTMLElement>(
687
- '[data-heading-id]',
688
- )
1281
+ const firstHeading
1282
+ = previewHost.querySelector<HTMLElement>('[data-heading-id]')
689
1283
  if (!firstHeading) {
690
1284
  showPinnedTitle = true
691
1285
  return
@@ -700,7 +1294,7 @@ ${safeCode}
700
1294
  syncActiveHeadingFromScroll()
701
1295
  syncPinnedTitleVisibility()
702
1296
  if (selectionToolbarVisible) {
703
- closeSelectionToolbar()
1297
+ queueSelectionRefresh(true)
704
1298
  }
705
1299
  ondocumentscroll?.(event)
706
1300
  }
@@ -724,7 +1318,7 @@ ${safeCode}
724
1318
  * 选区工具条依赖真实 DOM 选区范围,因此只在 selection 落在当前正文容器内时显示。
725
1319
  * 一旦内容刷新或滚动位置变化,就主动关闭,避免把旧选区动作误用到新内容上。
726
1320
  */
727
- function handleSelectionChange(): void {
1321
+ function handleSelectionChange(remeasured = false): void {
728
1322
  if (
729
1323
  !selectionToolsEnabled
730
1324
  || typeof window === 'undefined'
@@ -734,79 +1328,70 @@ ${safeCode}
734
1328
  return
735
1329
  }
736
1330
 
737
- const selection = window.getSelection()
738
- if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
739
- closeSelectionToolbar()
740
- return
741
- }
742
-
743
- const anchorNode = selection.anchorNode
744
- const focusNode = selection.focusNode
745
- if (
746
- !anchorNode
747
- || !focusNode
748
- || !previewHost.contains(anchorNode)
749
- || !previewHost.contains(focusNode)
750
- ) {
751
- closeSelectionToolbar()
752
- return
753
- }
1331
+ clearPendingSelectionToolbarClose()
754
1332
 
755
- const text = selection.toString().trim()
756
- if (!text) {
757
- closeSelectionToolbar()
1333
+ const previewSelection = readPreviewSelectionState()
1334
+ if (!previewSelection) {
1335
+ scheduleSelectionToolbarClose()
758
1336
  return
759
1337
  }
760
1338
 
761
- const range = selection.getRangeAt(0)
1339
+ const { selection, range, text } = previewSelection
762
1340
  const rect = range.getBoundingClientRect()
763
1341
  const hostRect = editorScrollHost.getBoundingClientRect()
764
1342
  // 浮层尺寸在首次渲染前拿不到精确高度,这里用稳定的经验值决定上下避让方向,避免直接盖住选中文本。
765
- const estimatedToolbarHeight = rewriteMenuOpen ? 132 : 56
1343
+ const estimatedToolbarHeight = 60
766
1344
  const offset = 12
767
1345
  const topSpace = rect.top - hostRect.top
768
1346
  const bottomSpace = hostRect.bottom - rect.bottom
769
- const placement
770
- = topSpace >= estimatedToolbarHeight + offset || bottomSpace < estimatedToolbarHeight
1347
+ const placement: SelectionToolbarPosition['placement']
1348
+ = topSpace >= estimatedToolbarHeight + offset
1349
+ || bottomSpace < estimatedToolbarHeight
771
1350
  ? 'top'
772
1351
  : 'bottom'
773
1352
  const center = rect.left - hostRect.left + rect.width / 2
774
1353
  const horizontalPadding = 16
1354
+ const measuredToolbarHalfWidth = selectionToolbarEl
1355
+ ? selectionToolbarEl.offsetWidth / 2
1356
+ : 320
775
1357
  const estimatedToolbarHalfWidth = Math.min(
776
- 272,
777
- Math.max(
778
- 132,
779
- (editorScrollHost.clientWidth - horizontalPadding * 2) / 2,
780
- ),
1358
+ Math.max(132, measuredToolbarHalfWidth),
1359
+ Math.max(132, (editorScrollHost.clientWidth - horizontalPadding * 2) / 2),
781
1360
  )
782
1361
  const minCenter = horizontalPadding + estimatedToolbarHalfWidth
783
1362
  const maxCenter = editorScrollHost.clientWidth - minCenter
784
- const alignment
1363
+ const alignment: SelectionToolbarPosition['alignment']
785
1364
  = center <= minCenter
786
1365
  ? 'left'
787
1366
  : center >= maxCenter
788
1367
  ? 'right'
789
1368
  : 'center'
790
- const toolbarLeft
791
- = alignment === 'left'
792
- ? horizontalPadding
793
- : alignment === 'right'
794
- ? editorScrollHost.clientWidth - horizontalPadding
795
- : center
796
- const toolbarTop
797
- = placement === 'top'
798
- ? editorScrollHost.scrollTop + rect.top - hostRect.top - estimatedToolbarHeight - offset
799
- : editorScrollHost.scrollTop + rect.bottom - hostRect.top + offset
1369
+ const toolbarLeft = alignment === 'left'
1370
+ ? horizontalPadding
1371
+ : alignment === 'right'
1372
+ ? editorScrollHost.clientWidth - horizontalPadding
1373
+ : center
1374
+ const toolbarTop = placement === 'top'
1375
+ ? editorScrollHost.scrollTop + rect.top - hostRect.top - estimatedToolbarHeight - offset
1376
+ : editorScrollHost.scrollTop + rect.bottom - hostRect.top + offset
800
1377
 
801
1378
  selectedText = text
802
1379
  selectionToolbarVisible = true
803
- rewriteMenuOpen = false
1380
+ selectionFormatState = readSelectionFormatState(selection)
804
1381
  toolbarPosition = {
805
1382
  top: Math.max(12, toolbarTop),
806
1383
  left: Math.max(horizontalPadding, toolbarLeft),
807
1384
  placement,
808
1385
  alignment,
809
1386
  }
1387
+
1388
+ if (activeSelectionMenu) {
1389
+ updateSelectionMenuAnchor(activeSelectionMenu)
1390
+ }
1391
+
1392
+ if (!remeasured) {
1393
+ scheduleSelectionToolbarMeasurement()
1394
+ }
810
1395
  }
811
1396
 
812
1397
  async function applyRewrite(actionId: string): Promise<void> {
@@ -814,7 +1399,8 @@ ${safeCode}
814
1399
  return
815
1400
  }
816
1401
 
817
- rewriteMenuOpen = false
1402
+ activeSelectionMenu = null
1403
+ activeSelectionMenuTrigger = null
818
1404
  await onrewrite({
819
1405
  actionId,
820
1406
  selectedText,
@@ -825,26 +1411,63 @@ ${safeCode}
825
1411
 
826
1412
  function applyBlockFormat(kind: MarkdownBlockFormatKind): void {
827
1413
  onapplyblockformat?.(kind)
828
- closeSelectionToolbar()
1414
+ activeSelectionMenu = null
1415
+ activeSelectionMenuTrigger = null
1416
+ queueSelectionRefresh()
1417
+ }
1418
+
1419
+ function applyBlockStyle(kind: MarkdownBlockStyleKind): void {
1420
+ onapplyblockstyle?.(kind)
1421
+ activeSelectionMenu = null
1422
+ activeSelectionMenuTrigger = null
1423
+ queueSelectionRefresh()
829
1424
  }
830
1425
 
831
1426
  function applyInlineFormat(kind: MarkdownInlineFormatKind): void {
832
1427
  onapplyinlineformat?.(kind)
833
- closeSelectionToolbar()
1428
+ queueSelectionRefresh()
1429
+ }
1430
+
1431
+ function applyAlignment(kind: MarkdownTextAlignKind): void {
1432
+ onapplyalignment?.(kind)
1433
+ activeSelectionMenu = null
1434
+ activeSelectionMenuTrigger = null
1435
+ queueSelectionRefresh()
1436
+ }
1437
+
1438
+ function applyLink(): void {
1439
+ onapplylink?.(linkDraft.trim() || null)
1440
+ activeSelectionMenu = null
1441
+ activeSelectionMenuTrigger = null
1442
+ queueSelectionRefresh()
1443
+ }
1444
+
1445
+ function removeLink(): void {
1446
+ onapplylink?.(null)
1447
+ activeSelectionMenu = null
1448
+ activeSelectionMenuTrigger = null
1449
+ queueSelectionRefresh()
1450
+ }
1451
+
1452
+ function applyColor(request: MarkdownColorFormatRequest): void {
1453
+ onapplycolor?.(request)
1454
+ queueSelectionRefresh()
834
1455
  }
835
1456
 
836
1457
  async function copySelection(): Promise<void> {
837
1458
  await oncopyselection?.()
838
- closeSelectionToolbar()
1459
+ activeSelectionMenu = null
1460
+ activeSelectionMenuTrigger = null
839
1461
  }
840
1462
 
841
1463
  async function annotateSelection(): Promise<void> {
842
1464
  await onannotation?.()
843
- closeSelectionToolbar()
1465
+ activeSelectionMenu = null
1466
+ activeSelectionMenuTrigger = null
844
1467
  }
845
1468
 
846
1469
  function handlePreviewMouseUp(): void {
847
- handleSelectionChange()
1470
+ queueSelectionRefresh()
848
1471
  onpreviewmouseup?.()
849
1472
  }
850
1473
 
@@ -853,7 +1476,7 @@ ${safeCode}
853
1476
  }
854
1477
 
855
1478
  function handlePreviewBlur(): void {
856
- closeSelectionToolbar()
1479
+ queueSelectionRefresh(true)
857
1480
  onpreviewblur?.()
858
1481
  }
859
1482
  </script>
@@ -893,6 +1516,7 @@ ${safeCode}
893
1516
  type='button'
894
1517
  class='hai-ai-doc-toolbar-icon'
895
1518
  aria-label={uiM('markdown_undo')}
1519
+ title={`${uiM('markdown_undo')} (Ctrl/Cmd+Z)`}
896
1520
  disabled={undoDisabled}
897
1521
  onclick={onundo}
898
1522
  >
@@ -909,6 +1533,7 @@ ${safeCode}
909
1533
  type='button'
910
1534
  class='hai-ai-doc-toolbar-icon'
911
1535
  aria-label={uiM('markdown_redo')}
1536
+ title={`${uiM('markdown_redo')} (Ctrl/Cmd+Shift+Z)`}
912
1537
  disabled={redoDisabled}
913
1538
  onclick={onredo}
914
1539
  >
@@ -951,12 +1576,12 @@ ${safeCode}
951
1576
  </button>
952
1577
 
953
1578
  <AiDocumentDownloadMenu
954
- content={content}
955
- title={title}
956
- sourceKind={sourceKind}
957
- codeLanguage={codeLanguage}
1579
+ {content}
1580
+ {title}
1581
+ {sourceKind}
1582
+ {codeLanguage}
958
1583
  actions={downloadActions}
959
- ondownload={ondownload}
1584
+ {ondownload}
960
1585
  showLabel={true}
961
1586
  iconOnly={false}
962
1587
  triggerTitle={uiM('markdown_download')}
@@ -996,7 +1621,7 @@ ${safeCode}
996
1621
  {/if}
997
1622
 
998
1623
  <div
999
- class:hai-ai-doc-layout-collapsed={outlineCollapsed}
1624
+ class:hai-ai-doc-layout-collapsed={!showOutline || outlineCollapsed}
1000
1625
  class='hai-ai-doc-layout'
1001
1626
  >
1002
1627
  {#if showOutline && !outlineCollapsed}
@@ -1046,14 +1671,7 @@ ${safeCode}
1046
1671
  class='hai-ai-doc-scroll'
1047
1672
  onclick={handleClick}
1048
1673
  onscroll={handleDocumentScroll}
1049
- onkeyup={handleSelectionChange}
1050
- onfocusout={() => {
1051
- if (typeof window !== 'undefined') {
1052
- window.setTimeout(() => {
1053
- handleSelectionChange()
1054
- }, 0)
1055
- }
1056
- }}
1674
+ onkeyup={() => handleSelectionChange()}
1057
1675
  >
1058
1676
  {#if selectionToolbarVisible}
1059
1677
  <div
@@ -1063,189 +1681,521 @@ ${safeCode}
1063
1681
  style={`top:${toolbarPosition.top}px; left:${toolbarPosition.left}px;`}
1064
1682
  >
1065
1683
  <div
1066
- class='hai-ai-doc-selection-toolbar'
1067
- role='toolbar'
1068
- tabindex='-1'
1069
- onmousedown={event => event.preventDefault()}
1684
+ class='hai-ai-doc-selection-chrome'
1070
1685
  >
1071
- {#if onrewrite}
1072
- <button
1073
- type='button'
1074
- class='hai-ai-doc-selection-chip'
1075
- disabled={rewritePending}
1076
- onclick={() => (rewriteMenuOpen = !rewriteMenuOpen)}
1077
- >
1078
- <svg viewBox='0 0 24 24' aria-hidden='true'>
1079
- <path
1080
- d='M12 3.25a.75.75 0 0 1 .72.55l1.06 3.62 3.63 1.06a.75.75 0 0 1 0 1.44l-3.63 1.06-1.06 3.63a.75.75 0 0 1-1.44 0l-1.06-3.63-3.63-1.06a.75.75 0 0 1 0-1.44l3.63-1.06 1.06-3.62a.75.75 0 0 1 .72-.55Zm6.5 11.5a.75.75 0 0 1 .72.55l.42 1.43 1.43.42a.75.75 0 0 1 0 1.44l-1.43.42-.42 1.43a.75.75 0 0 1-1.44 0l-.42-1.43-1.43-.42a.75.75 0 0 1 0-1.44l1.43-.42.42-1.43a.75.75 0 0 1 .72-.55Z'
1081
- ></path>
1082
- </svg>
1083
- <span>{uiM('markdown_rewrite')}</span>
1084
- </button>
1085
- {/if}
1086
-
1087
- {#if onapplyblockformat}
1088
- {#if onrewrite}
1089
- <span class='hai-ai-doc-selection-divider'></span>
1090
- {/if}
1091
- <button
1092
- type='button'
1093
- class='hai-ai-doc-selection-btn'
1094
- title={uiM('markdown_format_heading')}
1095
- onclick={() => applyBlockFormat('heading')}
1096
- >
1097
- <svg viewBox='0 0 24 24' aria-hidden='true'>
1098
- <path
1099
- d='M5 5.25a.75.75 0 0 1 .75.75V11h6.5V6a.75.75 0 0 1 1.5 0v12a.75.75 0 0 1-1.5 0v-5.5h-6.5V18a.75.75 0 0 1-1.5 0V6A.75.75 0 0 1 5 5.25Zm11.25 2.5a.75.75 0 0 1 0 1.5h3a.75.75 0 0 1 0 1.5H18.5v8.5a.75.75 0 0 1-1.5 0v-8.5h-.75a.75.75 0 0 1 0-1.5h3Z'
1100
- ></path>
1101
- </svg>
1102
- </button>
1103
- <button
1104
- type='button'
1105
- class='hai-ai-doc-selection-btn'
1106
- title={uiM('markdown_format_bullet')}
1107
- onclick={() => applyBlockFormat('bullet')}
1108
- >
1109
- <svg viewBox='0 0 24 24' aria-hidden='true'>
1110
- <path
1111
- d='M6.25 7.25a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm14 0a.75.75 0 0 1-.75.75H8.75a.75.75 0 0 1 0-1.5H19.5a.75.75 0 0 1 .75.75Zm-14 5.5a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm14 0a.75.75 0 0 1-.75.75H8.75a.75.75 0 0 1 0-1.5H19.5a.75.75 0 0 1 .75.75Zm-14 5.5a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm14 0a.75.75 0 0 1-.75.75H8.75a.75.75 0 0 1 0-1.5H19.5a.75.75 0 0 1 .75.75Z'
1112
- ></path>
1113
- </svg>
1114
- </button>
1115
- {/if}
1116
-
1117
- {#if onapplyinlineformat}
1118
- {#if onapplyblockformat}
1119
- <span class='hai-ai-doc-selection-divider'></span>
1120
- {/if}
1121
- <button
1122
- type='button'
1123
- class='hai-ai-doc-selection-btn'
1124
- title={uiM('markdown_format_bold')}
1125
- onclick={() => applyInlineFormat('bold')}
1126
- >
1127
- <strong>B</strong>
1128
- </button>
1129
- <button
1130
- type='button'
1131
- class='hai-ai-doc-selection-btn'
1132
- title={uiM('markdown_format_strike')}
1133
- onclick={() => applyInlineFormat('strike')}
1134
- >
1135
- S
1136
- </button>
1137
- <button
1138
- type='button'
1139
- class='hai-ai-doc-selection-btn'
1140
- title={uiM('markdown_format_italic')}
1141
- onclick={() => applyInlineFormat('italic')}
1142
- >
1143
- <em>I</em>
1144
- </button>
1145
- <button
1146
- type='button'
1147
- class='hai-ai-doc-selection-btn'
1148
- title={uiM('markdown_format_underline')}
1149
- onclick={() => applyInlineFormat('underline')}
1150
- >
1151
- U
1152
- </button>
1153
- <button
1154
- type='button'
1155
- class='hai-ai-doc-selection-btn'
1156
- title={uiM('markdown_format_link')}
1157
- onclick={() => applyInlineFormat('link')}
1158
- >
1159
- <svg viewBox='0 0 24 24' aria-hidden='true'>
1160
- <path
1161
- d='M7.78 15.72a3.75 3.75 0 0 1 0-5.3l2.47-2.47a3.75 3.75 0 0 1 5.3 5.3l-.97.98a.75.75 0 0 1-1.06-1.06l.98-.97a2.25 2.25 0 0 0-3.19-3.19l-2.47 2.47a2.25 2.25 0 1 0 3.18 3.18l.49-.49a.75.75 0 0 1 1.06 1.06l-.49.49a3.75 3.75 0 0 1-5.3 0Zm8.44-7.44a.75.75 0 0 1 0 1.06l-8 8a.75.75 0 0 1-1.06-1.06l8-8a.75.75 0 0 1 1.06 0Z'
1162
- ></path>
1163
- </svg>
1164
- </button>
1165
- <button
1166
- type='button'
1167
- class='hai-ai-doc-selection-btn'
1168
- title={uiM('markdown_format_code')}
1169
- onclick={() => applyInlineFormat('code')}
1170
- >
1171
- &lt;/&gt;
1172
- </button>
1173
- <button
1174
- type='button'
1175
- class='hai-ai-doc-selection-btn'
1176
- title={uiM('markdown_format_highlight')}
1177
- onclick={() => applyInlineFormat('highlight')}
1178
- >
1179
- <span class='hai-ai-doc-selection-highlight-icon'>A</span>
1180
- </button>
1181
- {/if}
1182
-
1183
- {#if oncopyselection || onannotation}
1184
- {#if onapplyinlineformat}
1185
- <span class='hai-ai-doc-selection-divider'></span>
1186
- {/if}
1187
- {/if}
1188
-
1189
- {#if oncopyselection}
1190
- <button
1191
- type='button'
1192
- class='hai-ai-doc-selection-btn hai-ai-doc-selection-btn-wide'
1193
- title={uiM('markdown_copy_selection')}
1194
- onclick={copySelection}
1195
- >
1196
- <svg viewBox='0 0 24 24' aria-hidden='true'>
1197
- <path
1198
- d='M8.75 4.25A2.75 2.75 0 0 0 6 7v8.25A2.75 2.75 0 0 0 8.75 18h7.5A2.75 2.75 0 0 0 19 15.25V7a2.75 2.75 0 0 0-2.75-2.75h-7.5Zm-4 3A2.75 2.75 0 0 1 7.5 4.5a.75.75 0 0 0 0-1.5A4.25 4.25 0 0 0 3.25 7.25v8.5A4.25 4.25 0 0 0 7.5 20a.75.75 0 0 0 0-1.5 2.75 2.75 0 0 1-2.75-2.75v-8.5Z'
1199
- ></path>
1200
- </svg>
1201
- </button>
1202
- {/if}
1203
-
1204
- {#if onannotation}
1205
- <button
1206
- type='button'
1207
- class='hai-ai-doc-selection-btn hai-ai-doc-selection-btn-wide'
1208
- title={uiM('markdown_annotation')}
1209
- onclick={annotateSelection}
1210
- >
1211
- <svg viewBox='0 0 24 24' aria-hidden='true'>
1212
- <path
1213
- d='M6.75 4.25A2.75 2.75 0 0 0 4 7v7.75A2.75 2.75 0 0 0 6.75 17.5h1.72l2.3 2.01a1.75 1.75 0 0 0 2.3 0l2.3-2.01h1.88A2.75 2.75 0 0 0 20 14.75V7a2.75 2.75 0 0 0-2.75-2.75H6.75Zm1.5 4a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 0 1.5H9a.75.75 0 0 1-.75-.75Zm0 3.5A.75.75 0 0 1 9 11h4a.75.75 0 0 1 0 1.5H9a.75.75 0 0 1-.75-.75Z'
1214
- ></path>
1215
- </svg>
1216
- </button>
1217
- {/if}
1218
- </div>
1219
-
1220
- {#if rewriteMenuOpen}
1221
1686
  <div
1222
- class='hai-ai-doc-rewrite-menu'
1223
- role='menu'
1687
+ bind:this={selectionToolbarEl}
1688
+ class='hai-ai-doc-selection-toolbar'
1689
+ role='toolbar'
1224
1690
  tabindex='-1'
1225
1691
  onmousedown={event => event.preventDefault()}
1226
1692
  >
1227
- {#each resolvedRewriteActions as action}
1693
+ {#if onrewrite}
1228
1694
  <button
1229
1695
  type='button'
1696
+ class={cn(
1697
+ 'hai-ai-doc-selection-chip',
1698
+ (!directRewriteAction && activeSelectionMenu === 'rewrite')
1699
+ ? 'hai-ai-doc-selection-chip--active'
1700
+ : '',
1701
+ )}
1230
1702
  disabled={rewritePending}
1231
- onclick={() => applyRewrite(action.id)}
1703
+ onclick={(event) => {
1704
+ if (directRewriteAction) {
1705
+ void applyRewrite(directRewriteAction.id)
1706
+ return
1707
+ }
1708
+
1709
+ toggleSelectionMenu('rewrite', event)
1710
+ }}
1232
1711
  >
1233
- {action.label}
1712
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1713
+ <path
1714
+ d='M12 3.25a.75.75 0 0 1 .72.55l1.06 3.62 3.63 1.06a.75.75 0 0 1 0 1.44l-3.63 1.06-1.06 3.63a.75.75 0 0 1-1.44 0l-1.06-3.63-3.63-1.06a.75.75 0 0 1 0-1.44l3.63-1.06 1.06-3.62a.75.75 0 0 1 .72-.55Zm6.5 11.5a.75.75 0 0 1 .72.55l.42 1.43 1.43.42a.75.75 0 0 1 0 1.44l-1.43.42-.42 1.43a.75.75 0 0 1-1.44 0l-.42-1.43-1.43-.42a.75.75 0 0 1 0-1.44l1.43-.42.42-1.43a.75.75 0 0 1 .72-.55Z'
1715
+ ></path>
1716
+ </svg>
1717
+ <span>{uiM('markdown_rewrite')}</span>
1234
1718
  </button>
1235
- {/each}
1236
- </div>
1237
- {/if}
1238
- </div>
1239
- {/if}
1719
+ {/if}
1240
1720
 
1241
- <article
1242
- bind:this={previewHost}
1243
- class={readerDocumentClass}
1244
- contenteditable={editable}
1245
- role='document'
1246
- onmouseup={handlePreviewMouseUp}
1247
- oninput={handlePreviewInput}
1248
- onblur={handlePreviewBlur}
1721
+ {#if richBlockFormattingEnabled}
1722
+ {#if onrewrite}
1723
+ <span class='hai-ai-doc-selection-divider'></span>
1724
+ {/if}
1725
+ <button
1726
+ type='button'
1727
+ class={cn(
1728
+ 'hai-ai-doc-selection-trigger',
1729
+ activeSelectionMenu === 'block'
1730
+ ? 'hai-ai-doc-selection-trigger--active'
1731
+ : '',
1732
+ )}
1733
+ title={uiM('markdown_format_heading')}
1734
+ onclick={event => toggleSelectionMenu('block', event)}
1735
+ >
1736
+ <span class='hai-ai-doc-selection-trigger-label'>
1737
+ {activeBlockFormatOption.shortLabel}
1738
+ </span>
1739
+ <svg
1740
+ viewBox='0 0 24 24'
1741
+ aria-hidden='true'
1742
+ class='hai-ai-doc-selection-trigger-chevron'
1743
+ >
1744
+ <path
1745
+ d='M6.97 8.47a.75.75 0 0 1 1.06 0L12 12.44l3.97-3.97a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06 0l-4.5-4.5a.75.75 0 0 1 0-1.06Z'
1746
+ ></path>
1747
+ </svg>
1748
+ </button>
1749
+ {:else if legacyBlockFormattingEnabled}
1750
+ {#if onrewrite}
1751
+ <span class='hai-ai-doc-selection-divider'></span>
1752
+ {/if}
1753
+ <button
1754
+ type='button'
1755
+ class='hai-ai-doc-selection-btn'
1756
+ title={uiM('markdown_format_heading')}
1757
+ onclick={() => applyBlockFormat('heading')}
1758
+ >
1759
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1760
+ <path
1761
+ d='M5 5.25a.75.75 0 0 1 .75.75V11h6.5V6a.75.75 0 0 1 1.5 0v12a.75.75 0 0 1-1.5 0v-5.5h-6.5V18a.75.75 0 0 1-1.5 0V6A.75.75 0 0 1 5 5.25Zm11.25 2.5a.75.75 0 0 1 0 1.5h3a.75.75 0 0 1 0 1.5H18.5v8.5a.75.75 0 0 1-1.5 0v-8.5h-.75a.75.75 0 0 1 0-1.5h3Z'
1762
+ ></path>
1763
+ </svg>
1764
+ </button>
1765
+ <button
1766
+ type='button'
1767
+ class='hai-ai-doc-selection-btn'
1768
+ title={uiM('markdown_format_bullet')}
1769
+ onclick={() => applyBlockFormat('bullet')}
1770
+ >
1771
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1772
+ <path
1773
+ d='M6.25 7.25a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm14 0a.75.75 0 0 1-.75.75H8.75a.75.75 0 0 1 0-1.5H19.5a.75.75 0 0 1 .75.75Zm-14 5.5a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm14 0a.75.75 0 0 1-.75.75H8.75a.75.75 0 0 1 0-1.5H19.5a.75.75 0 0 1 .75.75Zm-14 5.5a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm14 0a.75.75 0 0 1-.75.75H8.75a.75.75 0 0 1 0-1.5H19.5a.75.75 0 0 1 .75.75Z'
1774
+ ></path>
1775
+ </svg>
1776
+ </button>
1777
+ {/if}
1778
+
1779
+ {#if onapplyalignment}
1780
+ {#if anyBlockFormattingEnabled || onrewrite}
1781
+ <span class='hai-ai-doc-selection-divider'></span>
1782
+ {/if}
1783
+ <button
1784
+ type='button'
1785
+ class={cn(
1786
+ 'hai-ai-doc-selection-trigger',
1787
+ activeSelectionMenu === 'align'
1788
+ ? 'hai-ai-doc-selection-trigger--active'
1789
+ : '',
1790
+ )}
1791
+ title={uiM('markdown_align')}
1792
+ onclick={event => toggleSelectionMenu('align', event)}
1793
+ >
1794
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1795
+ {#if selectionFormatState.alignment === 'center'}
1796
+ <path d='M5 6.25a.75.75 0 0 1 .75-.75h12.5a.75.75 0 0 1 0 1.5H5.75A.75.75 0 0 1 5 6.25Zm2.75 4.75a.75.75 0 0 1 .75-.75h7a.75.75 0 0 1 0 1.5h-7a.75.75 0 0 1-.75-.75Zm-2 4.75a.75.75 0 0 1 .75-.75h11a.75.75 0 0 1 0 1.5h-11a.75.75 0 0 1-.75-.75Z'></path>
1797
+ {:else if selectionFormatState.alignment === 'right'}
1798
+ <path d='M5.75 5.5a.75.75 0 0 0 0 1.5h12.5a.75.75 0 0 0 0-1.5H5.75Zm4 4.75a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Zm-4 4.75a.75.75 0 0 0 0 1.5h12.5a.75.75 0 0 0 0-1.5H5.75Z'></path>
1799
+ {:else if selectionFormatState.alignment === 'justify'}
1800
+ <path d='M5 6.25a.75.75 0 0 1 .75-.75h12.5a.75.75 0 0 1 0 1.5H5.75A.75.75 0 0 1 5 6.25Zm0 4.75a.75.75 0 0 1 .75-.75h12.5a.75.75 0 0 1 0 1.5H5.75A.75.75 0 0 1 5 11Zm0 4.75a.75.75 0 0 1 .75-.75h12.5a.75.75 0 0 1 0 1.5H5.75A.75.75 0 0 1 5 15.75Z'></path>
1801
+ {:else}
1802
+ <path d='M5 6.25a.75.75 0 0 1 .75-.75h12.5a.75.75 0 0 1 0 1.5H5.75A.75.75 0 0 1 5 6.25Zm0 4.75a.75.75 0 0 1 .75-.75h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 5 11Zm0 4.75a.75.75 0 0 1 .75-.75h12.5a.75.75 0 0 1 0 1.5H5.75A.75.75 0 0 1 5 15.75Z'></path>
1803
+ {/if}
1804
+ </svg>
1805
+ <svg
1806
+ viewBox='0 0 24 24'
1807
+ aria-hidden='true'
1808
+ class='hai-ai-doc-selection-trigger-chevron'
1809
+ >
1810
+ <path
1811
+ d='M6.97 8.47a.75.75 0 0 1 1.06 0L12 12.44l3.97-3.97a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06 0l-4.5-4.5a.75.75 0 0 1 0-1.06Z'
1812
+ ></path>
1813
+ </svg>
1814
+ </button>
1815
+ {/if}
1816
+
1817
+ {#if onapplyinlineformat || onapplylink || onapplycolor}
1818
+ {#if anyBlockFormattingEnabled || onapplyalignment || onrewrite}
1819
+ <span class='hai-ai-doc-selection-divider'></span>
1820
+ {/if}
1821
+ {/if}
1822
+
1823
+ {#if onapplyinlineformat}
1824
+ <button
1825
+ type='button'
1826
+ class={cn(
1827
+ 'hai-ai-doc-selection-btn',
1828
+ selectionFormatState.bold
1829
+ ? 'hai-ai-doc-selection-btn--active'
1830
+ : '',
1831
+ )}
1832
+ title={uiM('markdown_format_bold')}
1833
+ onclick={() => applyInlineFormat('bold')}
1834
+ >
1835
+ <strong>B</strong>
1836
+ </button>
1837
+ <button
1838
+ type='button'
1839
+ class={cn(
1840
+ 'hai-ai-doc-selection-btn',
1841
+ selectionFormatState.strike
1842
+ ? 'hai-ai-doc-selection-btn--active'
1843
+ : '',
1844
+ )}
1845
+ title={uiM('markdown_format_strike')}
1846
+ onclick={() => applyInlineFormat('strike')}
1847
+ >
1848
+ S
1849
+ </button>
1850
+ <button
1851
+ type='button'
1852
+ class={cn(
1853
+ 'hai-ai-doc-selection-btn',
1854
+ selectionFormatState.italic
1855
+ ? 'hai-ai-doc-selection-btn--active'
1856
+ : '',
1857
+ )}
1858
+ title={uiM('markdown_format_italic')}
1859
+ onclick={() => applyInlineFormat('italic')}
1860
+ >
1861
+ <em>I</em>
1862
+ </button>
1863
+ <button
1864
+ type='button'
1865
+ class={cn(
1866
+ 'hai-ai-doc-selection-btn',
1867
+ selectionFormatState.underline
1868
+ ? 'hai-ai-doc-selection-btn--active'
1869
+ : '',
1870
+ )}
1871
+ title={uiM('markdown_format_underline')}
1872
+ onclick={() => applyInlineFormat('underline')}
1873
+ >
1874
+ U
1875
+ </button>
1876
+ {/if}
1877
+
1878
+ {#if onapplylink || onapplyinlineformat}
1879
+ <button
1880
+ type='button'
1881
+ class={cn(
1882
+ 'hai-ai-doc-selection-btn',
1883
+ selectionFormatState.linkHref
1884
+ ? 'hai-ai-doc-selection-btn--active'
1885
+ : '',
1886
+ activeSelectionMenu === 'link'
1887
+ ? 'hai-ai-doc-selection-btn--active'
1888
+ : '',
1889
+ )}
1890
+ title={uiM('markdown_format_link')}
1891
+ onclick={event =>
1892
+ onapplylink
1893
+ ? toggleSelectionMenu('link', event)
1894
+ : applyInlineFormat('link')}
1895
+ >
1896
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1897
+ <path
1898
+ d='M7.78 15.72a3.75 3.75 0 0 1 0-5.3l2.47-2.47a3.75 3.75 0 0 1 5.3 5.3l-.97.98a.75.75 0 0 1-1.06-1.06l.98-.97a2.25 2.25 0 0 0-3.19-3.19l-2.47 2.47a2.25 2.25 0 1 0 3.18 3.18l.49-.49a.75.75 0 0 1 1.06 1.06l-.49.49a3.75 3.75 0 0 1-5.3 0Zm8.44-7.44a.75.75 0 0 1 0 1.06l-8 8a.75.75 0 0 1-1.06-1.06l8-8a.75.75 0 0 1 1.06 0Z'
1899
+ ></path>
1900
+ </svg>
1901
+ </button>
1902
+ {/if}
1903
+
1904
+ {#if onapplyinlineformat}
1905
+ <button
1906
+ type='button'
1907
+ class={cn(
1908
+ 'hai-ai-doc-selection-btn',
1909
+ selectionFormatState.code
1910
+ ? 'hai-ai-doc-selection-btn--active'
1911
+ : '',
1912
+ )}
1913
+ title={uiM('markdown_format_code')}
1914
+ onclick={() => applyInlineFormat('code')}
1915
+ >
1916
+ &lt;/&gt;
1917
+ </button>
1918
+ <button
1919
+ type='button'
1920
+ class={cn(
1921
+ 'hai-ai-doc-selection-btn',
1922
+ selectionFormatState.highlight
1923
+ ? 'hai-ai-doc-selection-btn--active'
1924
+ : '',
1925
+ )}
1926
+ title={uiM('markdown_format_highlight')}
1927
+ onclick={() => applyInlineFormat('highlight')}
1928
+ >
1929
+ <span class='hai-ai-doc-selection-highlight-icon'>A</span>
1930
+ </button>
1931
+ {/if}
1932
+
1933
+ {#if onapplycolor}
1934
+ <button
1935
+ type='button'
1936
+ class={cn(
1937
+ 'hai-ai-doc-selection-trigger',
1938
+ activeSelectionMenu === 'color'
1939
+ ? 'hai-ai-doc-selection-trigger--active'
1940
+ : '',
1941
+ )}
1942
+ style={`--hai-ai-doc-current-color:${selectionFormatState.textColor ?? '#0f172a'};`}
1943
+ title={uiM('markdown_text_color')}
1944
+ onclick={event => toggleSelectionMenu('color', event)}
1945
+ >
1946
+ <span class='hai-ai-doc-selection-highlight-icon'>A</span>
1947
+ <svg
1948
+ viewBox='0 0 24 24'
1949
+ aria-hidden='true'
1950
+ class='hai-ai-doc-selection-trigger-chevron'
1951
+ >
1952
+ <path
1953
+ d='M6.97 8.47a.75.75 0 0 1 1.06 0L12 12.44l3.97-3.97a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06 0l-4.5-4.5a.75.75 0 0 1 0-1.06Z'
1954
+ ></path>
1955
+ </svg>
1956
+ </button>
1957
+ {/if}
1958
+
1959
+ {#if oncopyselection || onannotation}
1960
+ {#if onapplyinlineformat || onapplylink || onapplycolor}
1961
+ <span class='hai-ai-doc-selection-divider'></span>
1962
+ {/if}
1963
+ {/if}
1964
+
1965
+ {#if oncopyselection}
1966
+ <button
1967
+ type='button'
1968
+ class='hai-ai-doc-selection-btn hai-ai-doc-selection-btn-wide'
1969
+ title={uiM('markdown_copy_selection')}
1970
+ onclick={copySelection}
1971
+ >
1972
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1973
+ <path
1974
+ d='M8.75 4.25A2.75 2.75 0 0 0 6 7v8.25A2.75 2.75 0 0 0 8.75 18h7.5A2.75 2.75 0 0 0 19 15.25V7a2.75 2.75 0 0 0-2.75-2.75h-7.5Zm-4 3A2.75 2.75 0 0 1 7.5 4.5a.75.75 0 0 0 0-1.5A4.25 4.25 0 0 0 3.25 7.25v8.5A4.25 4.25 0 0 0 7.5 20a.75.75 0 0 0 0-1.5 2.75 2.75 0 0 1-2.75-2.75v-8.5Z'
1975
+ ></path>
1976
+ </svg>
1977
+ <span>{uiM('markdown_copy_selection')}</span>
1978
+ </button>
1979
+ {/if}
1980
+
1981
+ {#if onannotation}
1982
+ <button
1983
+ type='button'
1984
+ class='hai-ai-doc-selection-btn hai-ai-doc-selection-btn-wide'
1985
+ title={uiM('markdown_annotation')}
1986
+ onclick={annotateSelection}
1987
+ >
1988
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1989
+ <path
1990
+ d='M6.75 4.25A2.75 2.75 0 0 0 4 7v7.75A2.75 2.75 0 0 0 6.75 17.5h1.72l2.3 2.01a1.75 1.75 0 0 0 2.3 0l2.3-2.01h1.88A2.75 2.75 0 0 0 20 14.75V7a2.75 2.75 0 0 0-2.75-2.75H6.75Zm1.5 4a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 0 1.5H9a.75.75 0 0 1-.75-.75Zm0 3.5A.75.75 0 0 1 9 11h4a.75.75 0 0 1 0 1.5H9a.75.75 0 0 1-.75-.75Z'
1991
+ ></path>
1992
+ </svg>
1993
+ </button>
1994
+ {/if}
1995
+ </div>
1996
+
1997
+ {#if activeSelectionMenu === 'rewrite' && resolvedRewriteActions.length > 1}
1998
+ <div
1999
+ class='hai-ai-doc-rewrite-menu'
2000
+ data-menu-alignment={selectionMenuAlignment}
2001
+ role='menu'
2002
+ tabindex='-1'
2003
+ style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
2004
+ onmousedown={event => event.preventDefault()}
2005
+ >
2006
+ {#each resolvedRewriteActions as action}
2007
+ <button
2008
+ type='button'
2009
+ class='hai-ai-doc-rewrite-menu-btn'
2010
+ disabled={rewritePending}
2011
+ onclick={() => applyRewrite(action.id)}
2012
+ >
2013
+ {action.label}
2014
+ </button>
2015
+ {/each}
2016
+ </div>
2017
+ {:else if activeSelectionMenu === 'block' && richBlockFormattingEnabled}
2018
+ <div
2019
+ class='hai-ai-doc-selection-panel'
2020
+ data-menu-alignment={selectionMenuAlignment}
2021
+ role='menu'
2022
+ tabindex='-1'
2023
+ style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
2024
+ onmousedown={event => event.preventDefault()}
2025
+ >
2026
+ {#each BLOCK_FORMAT_OPTIONS as option}
2027
+ <button
2028
+ type='button'
2029
+ class={cn(
2030
+ 'hai-ai-doc-selection-menu-btn',
2031
+ selectionFormatState.blockFormat === option.value
2032
+ ? 'hai-ai-doc-selection-menu-btn--active'
2033
+ : '',
2034
+ )}
2035
+ onclick={() => applyBlockStyle(option.value)}
2036
+ >
2037
+ <span class='hai-ai-doc-selection-menu-label'>
2038
+ <span class='hai-ai-doc-selection-menu-short'>
2039
+ {option.shortLabel}
2040
+ </span>
2041
+ <span>{uiM(option.labelKey)}</span>
2042
+ </span>
2043
+ {#if selectionFormatState.blockFormat === option.value}
2044
+ <span class='hai-ai-doc-selection-menu-check'>✓</span>
2045
+ {/if}
2046
+ </button>
2047
+ {/each}
2048
+ </div>
2049
+ {:else if activeSelectionMenu === 'align' && onapplyalignment}
2050
+ <div
2051
+ class='hai-ai-doc-selection-panel'
2052
+ data-menu-alignment={selectionMenuAlignment}
2053
+ role='menu'
2054
+ tabindex='-1'
2055
+ style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
2056
+ onmousedown={event => event.preventDefault()}
2057
+ >
2058
+ {#each ALIGN_OPTIONS as option}
2059
+ <button
2060
+ type='button'
2061
+ class={cn(
2062
+ 'hai-ai-doc-selection-menu-btn',
2063
+ selectionFormatState.alignment === option.value
2064
+ ? 'hai-ai-doc-selection-menu-btn--active'
2065
+ : '',
2066
+ )}
2067
+ onclick={() => applyAlignment(option.value)}
2068
+ >
2069
+ <span>{uiM(option.labelKey)}</span>
2070
+ {#if selectionFormatState.alignment === option.value}
2071
+ <span class='hai-ai-doc-selection-menu-check'>✓</span>
2072
+ {/if}
2073
+ </button>
2074
+ {/each}
2075
+ </div>
2076
+ {:else if activeSelectionMenu === 'link' && onapplylink}
2077
+ <div
2078
+ class='hai-ai-doc-selection-panel hai-ai-doc-selection-panel--link'
2079
+ data-menu-alignment={selectionMenuAlignment}
2080
+ role='dialog'
2081
+ aria-label={uiM('markdown_link_dialog_label')}
2082
+ tabindex='-1'
2083
+ style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
2084
+ onmousedown={event => event.preventDefault()}
2085
+ >
2086
+ <input
2087
+ class='hai-ai-doc-selection-input'
2088
+ bind:value={linkDraft}
2089
+ aria-label={uiM('markdown_link_input_label')}
2090
+ placeholder={uiM('markdown_link_placeholder')}
2091
+ onkeydown={(event) => {
2092
+ if (event.key === 'Enter') {
2093
+ event.preventDefault()
2094
+ applyLink()
2095
+ }
2096
+ }}
2097
+ />
2098
+ <div class='hai-ai-doc-selection-panel-actions'>
2099
+ <button
2100
+ type='button'
2101
+ class='hai-ai-doc-selection-panel-btn hai-ai-doc-selection-panel-btn--primary'
2102
+ onclick={applyLink}
2103
+ >
2104
+ {uiM('markdown_link_apply')}
2105
+ </button>
2106
+ <button
2107
+ type='button'
2108
+ class='hai-ai-doc-selection-panel-btn'
2109
+ disabled={!selectionFormatState.linkHref}
2110
+ onclick={removeLink}
2111
+ >
2112
+ {uiM('markdown_link_remove')}
2113
+ </button>
2114
+ </div>
2115
+ </div>
2116
+ {:else if activeSelectionMenu === 'color' && onapplycolor}
2117
+ <div
2118
+ class='hai-ai-doc-selection-panel hai-ai-doc-selection-panel--color'
2119
+ data-menu-alignment={selectionMenuAlignment}
2120
+ role='dialog'
2121
+ aria-label={uiM('markdown_color_dialog_label')}
2122
+ tabindex='-1'
2123
+ style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
2124
+ onmousedown={event => event.preventDefault()}
2125
+ >
2126
+ <div class='hai-ai-doc-color-group'>
2127
+ <div class='hai-ai-doc-color-group-title'>
2128
+ {uiM('markdown_text_color')}
2129
+ </div>
2130
+ <div class='hai-ai-doc-color-grid'>
2131
+ {#each TEXT_COLOR_PRESETS as color}
2132
+ <button
2133
+ type='button'
2134
+ class={cn(
2135
+ 'hai-ai-doc-color-swatch',
2136
+ selectionFormatState.textColor === color
2137
+ ? 'hai-ai-doc-color-swatch--active'
2138
+ : '',
2139
+ )}
2140
+ title={color}
2141
+ onclick={() =>
2142
+ applyColor({ target: 'text', value: color })}
2143
+ >
2144
+ <span style={`background:${color};`}></span>
2145
+ </button>
2146
+ {/each}
2147
+ </div>
2148
+ </div>
2149
+ <div class='hai-ai-doc-color-group'>
2150
+ <div class='hai-ai-doc-color-group-title'>
2151
+ {uiM('markdown_background_color')}
2152
+ </div>
2153
+ <div class='hai-ai-doc-color-grid'>
2154
+ {#each BACKGROUND_COLOR_PRESETS as color}
2155
+ <button
2156
+ type='button'
2157
+ class={cn(
2158
+ 'hai-ai-doc-color-swatch',
2159
+ selectionFormatState.backgroundColor === color
2160
+ ? 'hai-ai-doc-color-swatch--active'
2161
+ : '',
2162
+ )}
2163
+ title={color}
2164
+ onclick={() =>
2165
+ applyColor({
2166
+ target: 'background',
2167
+ value: color,
2168
+ })}
2169
+ >
2170
+ <span style={`background:${color};`}></span>
2171
+ </button>
2172
+ {/each}
2173
+ </div>
2174
+ </div>
2175
+ <button
2176
+ type='button'
2177
+ class='hai-ai-doc-selection-panel-btn'
2178
+ onclick={() => {
2179
+ applyColor({ target: 'text', value: null })
2180
+ applyColor({ target: 'background', value: null })
2181
+ }}
2182
+ >
2183
+ {uiM('markdown_reset_default')}
2184
+ </button>
2185
+ </div>
2186
+ {/if}
2187
+ </div>
2188
+ </div>
2189
+ {/if}
2190
+
2191
+ <article
2192
+ bind:this={previewHost}
2193
+ class={readerDocumentClass}
2194
+ contenteditable={editable}
2195
+ role='document'
2196
+ onmouseup={handlePreviewMouseUp}
2197
+ oninput={handlePreviewInput}
2198
+ onblur={handlePreviewBlur}
1249
2199
  >
1250
2200
  <!-- eslint-disable-next-line svelte/no-at-html-tags -- Markdown HTML 渲染 -->
1251
2201
  {@html html}
@@ -1265,8 +2215,12 @@ ${safeCode}
1265
2215
  );
1266
2216
  --hai-ai-doc-selection-fg: oklch(var(--bc, 0.22 0 0));
1267
2217
  display: flex;
2218
+ width: 100%;
2219
+ min-width: 0;
1268
2220
  min-height: 0;
1269
2221
  height: 100%;
2222
+ container-type: inline-size;
2223
+ container-name: ai-doc-editor;
1270
2224
  flex-direction: column;
1271
2225
  border: 1px solid oklch(var(--bc) / 0.08);
1272
2226
  border-radius: 1.5rem;
@@ -1280,8 +2234,10 @@ ${safeCode}
1280
2234
  }
1281
2235
 
1282
2236
  .hai-ai-doc-shell {
1283
- display: grid;
1284
- grid-template-rows: auto auto 1fr;
2237
+ display: flex;
2238
+ flex-direction: column;
2239
+ width: 100%;
2240
+ min-width: 0;
1285
2241
  min-height: 0;
1286
2242
  height: 100%;
1287
2243
  }
@@ -1394,9 +2350,9 @@ ${safeCode}
1394
2350
  }
1395
2351
 
1396
2352
  .hai-ai-doc-toolbar-icon:not(:disabled):hover,
1397
- :global(.hai-ai-doc-toolbar-icon:hover),
2353
+ :global(.hai-ai-doc-toolbar-icon:not(:disabled):hover),
1398
2354
  .hai-ai-doc-toolbar-pill:not(:disabled):hover,
1399
- :global(.hai-ai-doc-toolbar-pill:hover),
2355
+ :global(.hai-ai-doc-toolbar-pill:not(:disabled):hover),
1400
2356
  .hai-ai-doc-toolbar-action:hover,
1401
2357
  .hai-ai-doc-toolbar-close:not(:disabled):hover,
1402
2358
  .hai-ai-doc-version-toggle:hover,
@@ -1407,9 +2363,9 @@ ${safeCode}
1407
2363
  }
1408
2364
 
1409
2365
  .hai-ai-doc-toolbar-icon:not(:disabled):hover,
1410
- :global(.hai-ai-doc-toolbar-icon:hover),
2366
+ :global(.hai-ai-doc-toolbar-icon:not(:disabled):hover),
1411
2367
  .hai-ai-doc-toolbar-pill:not(:disabled):hover,
1412
- :global(.hai-ai-doc-toolbar-pill:hover),
2368
+ :global(.hai-ai-doc-toolbar-pill:not(:disabled):hover),
1413
2369
  .hai-ai-doc-toolbar-close:not(:disabled):hover {
1414
2370
  transform: translateY(-1px);
1415
2371
  box-shadow: 0 12px 28px -22px oklch(var(--bc) / 0.36);
@@ -1420,11 +2376,25 @@ ${safeCode}
1420
2376
  .hai-ai-doc-toolbar-pill:disabled,
1421
2377
  :global(.hai-ai-doc-toolbar-pill:disabled),
1422
2378
  .hai-ai-doc-toolbar-close:disabled {
1423
- opacity: 0.42;
2379
+ opacity: 0.72;
2380
+ color: oklch(var(--bc) / 0.36);
2381
+ border-color: oklch(var(--bc) / 0.12);
2382
+ background: oklch(var(--b2) / 0.58);
1424
2383
  cursor: not-allowed;
1425
2384
  box-shadow: none;
1426
2385
  }
1427
2386
 
2387
+ .hai-ai-doc-toolbar-icon:disabled:hover,
2388
+ :global(.hai-ai-doc-toolbar-icon:disabled:hover),
2389
+ .hai-ai-doc-toolbar-pill:disabled:hover,
2390
+ :global(.hai-ai-doc-toolbar-pill:disabled:hover),
2391
+ .hai-ai-doc-toolbar-close:disabled:hover {
2392
+ cursor: not-allowed;
2393
+ transform: none;
2394
+ box-shadow: none;
2395
+ filter: grayscale(0.18);
2396
+ }
2397
+
1428
2398
  .hai-ai-doc-toolbar-action,
1429
2399
  .hai-ai-doc-version-toggle {
1430
2400
  border: 1px solid oklch(var(--bc) / 0.08);
@@ -1461,18 +2431,23 @@ ${safeCode}
1461
2431
  }
1462
2432
 
1463
2433
  .hai-ai-doc-layout {
2434
+ width: 100%;
1464
2435
  min-height: 0;
1465
- display: grid;
1466
- grid-template-columns: 220px minmax(0, 1fr);
2436
+ min-width: 0;
2437
+ flex: 1 1 auto;
2438
+ display: flex;
2439
+ align-items: stretch;
1467
2440
  position: relative;
1468
2441
  overflow: hidden;
1469
2442
  }
1470
2443
 
1471
2444
  .hai-ai-doc-layout.hai-ai-doc-layout-collapsed {
1472
- grid-template-columns: minmax(0, 1fr);
2445
+ display: flex;
1473
2446
  }
1474
2447
 
1475
2448
  .hai-ai-doc-outline {
2449
+ width: 220px;
2450
+ flex: 0 0 220px;
1476
2451
  min-height: 0;
1477
2452
  display: grid;
1478
2453
  grid-template-rows: auto 1fr;
@@ -1571,62 +2546,92 @@ ${safeCode}
1571
2546
  }
1572
2547
 
1573
2548
  .hai-ai-doc-reader {
2549
+ width: 100%;
2550
+ flex: 1 1 auto;
1574
2551
  min-width: 0;
1575
2552
  min-height: 0;
1576
2553
  position: relative;
1577
- z-index: 1;
2554
+ z-index: 2;
2555
+ isolation: isolate;
2556
+ display: flex;
2557
+ flex-direction: column;
1578
2558
  }
1579
2559
 
1580
2560
  .hai-ai-doc-scroll {
1581
2561
  position: relative;
2562
+ width: 100%;
2563
+ min-width: 0;
1582
2564
  height: 100%;
1583
2565
  overflow: auto;
1584
2566
  padding: 0.5rem 0 1.75rem;
2567
+ display: flex;
2568
+ flex-direction: column;
2569
+ flex: 1 1 auto;
2570
+ min-height: 0;
1585
2571
  }
1586
2572
 
1587
2573
  .hai-ai-doc-selection-layer {
1588
2574
  position: absolute;
1589
- z-index: 30;
1590
- display: flex;
1591
- flex-direction: column;
1592
- gap: 0.45rem;
1593
- width: min(max-content, calc(100% - 2rem));
2575
+ z-index: 60;
2576
+ display: block;
2577
+ width: fit-content;
2578
+ max-width: calc(100% - 2rem);
1594
2579
  transform: translateX(-50%);
2580
+ transition:
2581
+ top 0.14s cubic-bezier(0.2, 0.85, 0.24, 1),
2582
+ left 0.14s cubic-bezier(0.2, 0.85, 0.24, 1),
2583
+ transform 0.14s cubic-bezier(0.2, 0.85, 0.24, 1);
1595
2584
  pointer-events: none;
1596
2585
  }
1597
2586
 
1598
- .hai-ai-doc-selection-layer[data-placement='top'] {
1599
- flex-direction: column-reverse;
1600
- }
1601
-
1602
2587
  .hai-ai-doc-selection-layer[data-alignment='left'] {
1603
- transform: none;
2588
+ transform: translateX(0);
2589
+ justify-items: start;
1604
2590
  }
1605
2591
 
1606
2592
  .hai-ai-doc-selection-layer[data-alignment='right'] {
1607
2593
  transform: translateX(-100%);
2594
+ justify-items: end;
2595
+ }
2596
+
2597
+ .hai-ai-doc-selection-layer[data-alignment='center'] {
2598
+ justify-items: center;
2599
+ }
2600
+
2601
+ .hai-ai-doc-selection-chrome {
2602
+ position: relative;
2603
+ display: inline-grid;
2604
+ width: fit-content;
2605
+ max-width: min(100%, calc(100vw - 2rem));
1608
2606
  }
1609
2607
 
1610
2608
  .hai-ai-doc-selection-toolbar {
1611
2609
  display: flex;
1612
2610
  align-items: center;
1613
2611
  gap: 0.25rem;
1614
- flex-wrap: wrap;
1615
- max-width: min(calc(100vw - 2rem), 34rem);
2612
+ flex-wrap: nowrap;
2613
+ width: fit-content;
2614
+ max-width: calc(100% - 0.75rem);
2615
+ white-space: nowrap;
1616
2616
  padding: 0.38rem;
1617
2617
  border-radius: 1.2rem;
1618
- background: #fff;
1619
- border: 1px solid color-mix(in srgb, oklch(var(--bc)) 9%, white 91%);
2618
+ background-color: rgb(255 255 255 / 0.96);
2619
+ background: color-mix(in srgb, oklch(var(--b1, 1 0 0)) 94%, white 6%);
2620
+ border: 1px solid rgb(148 163 184 / 0.34);
2621
+ border-color: color-mix(in srgb, oklch(var(--bc, 0.22 0 0)) 16%, white 84%);
1620
2622
  box-shadow:
1621
2623
  0 30px 60px -34px rgb(15 23 42 / 0.28),
1622
2624
  0 14px 26px -18px rgb(15 23 42 / 0.16),
1623
2625
  0 0 0 1px rgb(255 255 255 / 0.94) inset;
1624
2626
  pointer-events: auto;
1625
2627
  isolation: isolate;
2628
+ position: relative;
2629
+ z-index: 2;
1626
2630
  }
1627
2631
 
1628
2632
  .hai-ai-doc-selection-chip,
1629
- .hai-ai-doc-selection-btn {
2633
+ .hai-ai-doc-selection-btn,
2634
+ .hai-ai-doc-selection-trigger {
1630
2635
  min-height: 2.25rem;
1631
2636
  border-radius: 0.9rem;
1632
2637
  transition:
@@ -1640,6 +2645,7 @@ ${safeCode}
1640
2645
  display: inline-flex;
1641
2646
  align-items: center;
1642
2647
  gap: 0.48rem;
2648
+ flex-shrink: 0;
1643
2649
  padding: 0 0.88rem;
1644
2650
  border: 1px solid oklch(var(--p) / 0.16);
1645
2651
  background: oklch(var(--p) / 0.1);
@@ -1654,9 +2660,16 @@ ${safeCode}
1654
2660
  fill: currentColor;
1655
2661
  }
1656
2662
 
2663
+ .hai-ai-doc-selection-chip--active {
2664
+ border-color: oklch(var(--p) / 0.28);
2665
+ background: oklch(var(--p) / 0.16);
2666
+ color: oklch(var(--bc));
2667
+ }
2668
+
1657
2669
  .hai-ai-doc-selection-chip:not(:disabled):hover,
1658
2670
  .hai-ai-doc-selection-btn:hover,
1659
- .hai-ai-doc-rewrite-menu button:hover {
2671
+ .hai-ai-doc-selection-trigger:hover,
2672
+ .hai-ai-doc-rewrite-menu-btn:hover {
1660
2673
  transform: translateY(-1px);
1661
2674
  }
1662
2675
 
@@ -1668,6 +2681,7 @@ ${safeCode}
1668
2681
  .hai-ai-doc-selection-btn {
1669
2682
  width: 2.25rem;
1670
2683
  min-width: 2.25rem;
2684
+ flex-shrink: 0;
1671
2685
  display: inline-flex;
1672
2686
  align-items: center;
1673
2687
  justify-content: center;
@@ -1679,21 +2693,61 @@ ${safeCode}
1679
2693
  }
1680
2694
 
1681
2695
  .hai-ai-doc-selection-btn-wide {
1682
- min-width: 2.25rem;
2696
+ width: auto;
2697
+ min-width: 4.6rem;
2698
+ gap: 0.42rem;
2699
+ padding: 0 0.74rem;
2700
+ }
2701
+
2702
+ .hai-ai-doc-selection-trigger {
2703
+ min-width: 3.4rem;
2704
+ display: inline-flex;
2705
+ align-items: center;
2706
+ gap: 0.34rem;
2707
+ padding: 0 0.66rem;
2708
+ border: 1px solid transparent;
2709
+ background: transparent;
2710
+ color: oklch(var(--bc) / 0.78);
2711
+ cursor: pointer;
1683
2712
  }
1684
2713
 
1685
- .hai-ai-doc-selection-btn:hover {
2714
+ .hai-ai-doc-selection-btn:hover,
2715
+ .hai-ai-doc-selection-trigger:hover {
1686
2716
  border-color: oklch(var(--bc) / 0.1);
1687
2717
  background: oklch(var(--bc) / 0.045);
1688
2718
  color: oklch(var(--bc));
1689
2719
  }
1690
2720
 
2721
+ .hai-ai-doc-selection-btn--active,
2722
+ .hai-ai-doc-selection-trigger--active {
2723
+ border-color: oklch(var(--p) / 0.22);
2724
+ background: oklch(var(--p) / 0.12);
2725
+ color: oklch(var(--bc));
2726
+ }
2727
+
1691
2728
  .hai-ai-doc-selection-btn svg {
1692
2729
  width: 1rem;
1693
2730
  height: 1rem;
1694
2731
  fill: currentColor;
1695
2732
  }
1696
2733
 
2734
+ .hai-ai-doc-selection-trigger svg {
2735
+ width: 1rem;
2736
+ height: 1rem;
2737
+ fill: currentColor;
2738
+ }
2739
+
2740
+ .hai-ai-doc-selection-trigger-label {
2741
+ font-weight: 600;
2742
+ line-height: 1;
2743
+ }
2744
+
2745
+ .hai-ai-doc-selection-trigger-chevron {
2746
+ width: 0.8rem !important;
2747
+ height: 0.8rem !important;
2748
+ opacity: 0.72;
2749
+ }
2750
+
1697
2751
  .hai-ai-doc-selection-divider {
1698
2752
  width: 1px;
1699
2753
  height: 1.45rem;
@@ -1705,6 +2759,7 @@ ${safeCode}
1705
2759
  .hai-ai-doc-selection-highlight-icon {
1706
2760
  position: relative;
1707
2761
  font-weight: 700;
2762
+ color: var(--hai-ai-doc-current-color, currentColor);
1708
2763
  }
1709
2764
 
1710
2765
  .hai-ai-doc-selection-highlight-icon::after {
@@ -1715,41 +2770,213 @@ ${safeCode}
1715
2770
  bottom: -0.02rem;
1716
2771
  height: 0.36rem;
1717
2772
  border-radius: 9999px;
1718
- background: oklch(var(--wa, 0.9 0.14 90) / 0.45);
2773
+ background: color-mix(
2774
+ in srgb,
2775
+ var(--hai-ai-doc-current-color, oklch(var(--wa, 0.9 0.14 90))) 22%,
2776
+ white 78%
2777
+ );
1719
2778
  z-index: -1;
1720
2779
  }
1721
2780
 
2781
+ .hai-ai-doc-selection-panel,
1722
2782
  .hai-ai-doc-rewrite-menu {
1723
- width: 13.75rem;
2783
+ position: absolute;
2784
+ left: var(--hai-ai-doc-selection-menu-left, 0px);
2785
+ top: calc(100% + 0.45rem);
1724
2786
  display: grid;
1725
- gap: 0.375rem;
1726
- padding: 0.5rem;
1727
- border-radius: 1.125rem;
1728
- background: #fff;
1729
- border: 1px solid color-mix(in srgb, oklch(var(--bc)) 9%, white 91%);
2787
+ gap: 0.34rem;
2788
+ width: min(18rem, calc(100vw - 2rem));
2789
+ max-width: calc(100vw - 2rem);
2790
+ padding: 0.42rem;
2791
+ border-radius: 1rem;
2792
+ background-color: rgb(255 255 255 / 0.96);
2793
+ background: color-mix(in srgb, oklch(var(--b1, 1 0 0)) 94%, white 6%);
2794
+ border: 1px solid rgb(148 163 184 / 0.3);
2795
+ border-color: color-mix(in srgb, oklch(var(--bc, 0.22 0 0)) 14%, white 86%);
1730
2796
  box-shadow:
1731
- 0 30px 60px -34px rgb(15 23 42 / 0.28),
1732
- 0 14px 26px -18px rgb(15 23 42 / 0.16),
1733
- 0 0 0 1px rgb(255 255 255 / 0.94) inset;
2797
+ 0 24px 40px -28px rgb(15 23 42 / 0.25),
2798
+ 0 10px 20px -16px rgb(15 23 42 / 0.14);
1734
2799
  pointer-events: auto;
1735
2800
  isolation: isolate;
2801
+ z-index: 3;
1736
2802
  }
1737
2803
 
1738
- .hai-ai-doc-rewrite-menu button {
1739
- text-align: left;
1740
- border: 1px solid transparent;
1741
- border-radius: 0.875rem;
1742
- padding: 0.7rem 0.8rem;
1743
- color: oklch(var(--bc) / 0.9);
1744
- background: transparent;
2804
+ .hai-ai-doc-rewrite-menu {
2805
+ width: fit-content;
2806
+ max-width: min(22rem, calc(100vw - 2rem));
2807
+ grid-template-columns: repeat(auto-fit, minmax(7.2rem, 1fr));
2808
+ }
2809
+
2810
+ .hai-ai-doc-selection-layer[data-placement='bottom'] .hai-ai-doc-selection-panel,
2811
+ .hai-ai-doc-selection-layer[data-placement='bottom'] .hai-ai-doc-rewrite-menu {
2812
+ top: auto;
2813
+ bottom: calc(100% + 0.45rem);
2814
+ }
2815
+
2816
+ .hai-ai-doc-selection-panel[data-menu-alignment='left'],
2817
+ .hai-ai-doc-rewrite-menu[data-menu-alignment='left'] {
2818
+ transform: translateX(0);
2819
+ }
2820
+
2821
+ .hai-ai-doc-selection-panel[data-menu-alignment='center'],
2822
+ .hai-ai-doc-rewrite-menu[data-menu-alignment='center'] {
2823
+ transform: translateX(-50%);
2824
+ }
2825
+
2826
+ .hai-ai-doc-selection-panel[data-menu-alignment='right'],
2827
+ .hai-ai-doc-rewrite-menu[data-menu-alignment='right'] {
2828
+ transform: translateX(-100%);
2829
+ }
2830
+
2831
+ .hai-ai-doc-selection-panel--link,
2832
+ .hai-ai-doc-selection-panel--color {
2833
+ width: min(16rem, calc(100vw - 2rem));
2834
+ }
2835
+
2836
+ .hai-ai-doc-selection-menu-btn,
2837
+ .hai-ai-doc-selection-panel-btn,
2838
+ .hai-ai-doc-rewrite-menu-btn {
2839
+ min-height: 2rem;
2840
+ display: inline-flex;
2841
+ align-items: center;
2842
+ justify-content: space-between;
2843
+ gap: 0.6rem;
2844
+ width: 100%;
2845
+ padding: 0 0.72rem;
2846
+ border-radius: 0.8rem;
2847
+ border: 1px solid oklch(var(--bc) / 0.14);
2848
+ background: color-mix(in srgb, oklch(var(--b1)) 92%, white 8%);
2849
+ color: oklch(var(--bc) / 0.86);
2850
+ font-size: 0.82rem;
2851
+ line-height: 1;
1745
2852
  cursor: pointer;
2853
+ transition:
2854
+ background-color 0.14s ease,
2855
+ border-color 0.14s ease,
2856
+ color 0.14s ease,
2857
+ transform 0.14s ease;
2858
+ }
2859
+
2860
+ .hai-ai-doc-selection-menu-btn--active,
2861
+ .hai-ai-doc-selection-panel-btn--primary {
2862
+ border-color: oklch(var(--p) / 0.28);
2863
+ background: oklch(var(--p) / 0.12);
2864
+ color: oklch(var(--bc));
2865
+ }
2866
+
2867
+ .hai-ai-doc-selection-menu-btn:hover,
2868
+ .hai-ai-doc-selection-panel-btn:hover,
2869
+ .hai-ai-doc-rewrite-menu-btn:hover:not(:disabled) {
2870
+ border-color: oklch(var(--bc) / 0.22);
2871
+ background: color-mix(in srgb, oklch(var(--b2)) 68%, white 32%);
2872
+ color: oklch(var(--bc));
2873
+ transform: translateY(-1px);
2874
+ }
2875
+
2876
+ .hai-ai-doc-selection-menu-label {
2877
+ display: inline-flex;
2878
+ align-items: center;
2879
+ gap: 0.62rem;
2880
+ }
2881
+
2882
+ .hai-ai-doc-selection-menu-short {
2883
+ min-width: 2rem;
2884
+ color: oklch(var(--p) / 0.92);
2885
+ font-weight: 700;
2886
+ }
2887
+
2888
+ .hai-ai-doc-selection-menu-check {
2889
+ color: oklch(var(--p) / 0.88);
2890
+ font-weight: 700;
2891
+ }
2892
+
2893
+ .hai-ai-doc-selection-input {
2894
+ width: 100%;
2895
+ min-height: 2.25rem;
2896
+ padding: 0 0.78rem;
2897
+ border-radius: 0.85rem;
2898
+ border: 1px solid oklch(var(--bc) / 0.16);
2899
+ background: color-mix(in srgb, oklch(var(--b1)) 94%, white 6%);
2900
+ color: oklch(var(--bc));
2901
+ font-size: 0.84rem;
2902
+ outline: none;
1746
2903
  }
1747
2904
 
1748
- .hai-ai-doc-rewrite-menu button:disabled {
2905
+ .hai-ai-doc-selection-input:focus {
2906
+ border-color: oklch(var(--p) / 0.34);
2907
+ box-shadow: 0 0 0 3px oklch(var(--p) / 0.12);
2908
+ }
2909
+
2910
+ .hai-ai-doc-selection-panel-actions {
2911
+ display: flex;
2912
+ gap: 0.4rem;
2913
+ }
2914
+
2915
+ .hai-ai-doc-selection-panel-actions .hai-ai-doc-selection-panel-btn {
2916
+ flex: 1 1 0;
2917
+ }
2918
+
2919
+ .hai-ai-doc-selection-panel-btn:disabled,
2920
+ .hai-ai-doc-rewrite-menu-btn:disabled {
1749
2921
  opacity: 0.55;
1750
2922
  cursor: default;
1751
2923
  }
1752
2924
 
2925
+ .hai-ai-doc-color-group {
2926
+ display: grid;
2927
+ gap: 0.42rem;
2928
+ }
2929
+
2930
+ .hai-ai-doc-color-group-title {
2931
+ font-size: 0.78rem;
2932
+ font-weight: 600;
2933
+ color: oklch(var(--bc) / 0.68);
2934
+ }
2935
+
2936
+ .hai-ai-doc-color-grid {
2937
+ display: grid;
2938
+ grid-template-columns: repeat(4, minmax(0, 1fr));
2939
+ gap: 0.42rem;
2940
+ }
2941
+
2942
+ .hai-ai-doc-color-swatch {
2943
+ width: 100%;
2944
+ aspect-ratio: 1;
2945
+ display: inline-flex;
2946
+ align-items: center;
2947
+ justify-content: center;
2948
+ padding: 0.18rem;
2949
+ border-radius: 0.7rem;
2950
+ border: 1px solid oklch(var(--bc) / 0.12);
2951
+ background: color-mix(in srgb, oklch(var(--b1)) 92%, white 8%);
2952
+ cursor: pointer;
2953
+ transition:
2954
+ transform 0.14s ease,
2955
+ border-color 0.14s ease,
2956
+ box-shadow 0.14s ease;
2957
+ }
2958
+
2959
+ .hai-ai-doc-color-swatch:hover {
2960
+ transform: translateY(-1px);
2961
+ border-color: oklch(var(--bc) / 0.2);
2962
+ }
2963
+
2964
+ .hai-ai-doc-color-swatch--active {
2965
+ border-color: oklch(var(--p) / 0.34);
2966
+ box-shadow: 0 0 0 3px oklch(var(--p) / 0.12);
2967
+ }
2968
+
2969
+ .hai-ai-doc-color-swatch span {
2970
+ width: 100%;
2971
+ height: 100%;
2972
+ border-radius: 0.5rem;
2973
+ border: 1px solid rgb(15 23 42 / 0.08);
2974
+ }
2975
+
2976
+ .hai-ai-doc-rewrite-menu-btn {
2977
+ justify-content: center;
2978
+ }
2979
+
1753
2980
  .hai-markdown {
1754
2981
  line-height: 1.9;
1755
2982
  color: inherit;
@@ -1758,12 +2985,28 @@ ${safeCode}
1758
2985
  }
1759
2986
 
1760
2987
  .hai-markdown-document {
2988
+ width: 100%;
2989
+ min-width: 0;
1761
2990
  max-width: 52.5rem;
1762
2991
  margin: 0 auto;
1763
2992
  padding: 1.25rem 2rem 6rem;
1764
2993
  outline: none;
1765
2994
  }
1766
2995
 
2996
+ .hai-markdown-document.hai-markdown-code-only {
2997
+ max-width: none !important;
2998
+ width: 100% !important;
2999
+ min-width: 0;
3000
+ margin: 0 !important;
3001
+ height: 100%;
3002
+ min-height: 100%;
3003
+ padding: 0.5rem 0.75rem 0.75rem;
3004
+ display: flex;
3005
+ flex-direction: column;
3006
+ flex: 1 1 auto;
3007
+ box-sizing: border-box;
3008
+ }
3009
+
1767
3010
  :global(.hai-markdown-document ::selection) {
1768
3011
  background: var(--hai-ai-doc-selection-bg, rgb(59 130 246 / 0.22));
1769
3012
  color: var(--hai-ai-doc-selection-fg, inherit);
@@ -1829,6 +3072,10 @@ ${safeCode}
1829
3072
  margin-bottom: 1em;
1830
3073
  }
1831
3074
 
3075
+ .hai-markdown :global(.hai-md-align-block) {
3076
+ width: 100%;
3077
+ }
3078
+
1832
3079
  .hai-markdown :global(a) {
1833
3080
  color: oklch(var(--p));
1834
3081
  text-decoration: none;
@@ -1852,6 +3099,23 @@ ${safeCode}
1852
3099
  color: oklch(var(--bc) / 0.55);
1853
3100
  }
1854
3101
 
3102
+ .hai-markdown :global(u) {
3103
+ text-decoration-thickness: 0.08em;
3104
+ text-underline-offset: 0.14em;
3105
+ }
3106
+
3107
+ .hai-markdown :global(mark) {
3108
+ padding: 0 0.12em;
3109
+ border-radius: 0.25rem;
3110
+ background: oklch(var(--wa, 0.9 0.14 90) / 0.34);
3111
+ color: inherit;
3112
+ }
3113
+
3114
+ .hai-markdown :global(.hai-md-inline-style) {
3115
+ border-radius: 0.2rem;
3116
+ padding: 0 0.04em;
3117
+ }
3118
+
1855
3119
  .hai-markdown :global(:not(pre) > code) {
1856
3120
  padding: 0.2em 0.4em;
1857
3121
  margin: 0 0.1em;
@@ -1881,6 +3145,7 @@ ${safeCode}
1881
3145
  gap: 0.75rem;
1882
3146
  padding: 0.65rem 1rem;
1883
3147
  min-height: 2.5rem;
3148
+ line-height: 1.2;
1884
3149
  background: oklch(var(--n) / 0.94);
1885
3150
  border-bottom: 1px solid oklch(var(--nc) / 0.1);
1886
3151
  }
@@ -1889,6 +3154,7 @@ ${safeCode}
1889
3154
  min-width: 0;
1890
3155
  display: flex;
1891
3156
  align-items: center;
3157
+ gap: 0.55rem;
1892
3158
  }
1893
3159
 
1894
3160
  .hai-markdown :global(.hai-md-code-actions) {
@@ -1900,12 +3166,54 @@ ${safeCode}
1900
3166
  .hai-markdown :global(.hai-md-code-lang) {
1901
3167
  font-size: 0.75rem;
1902
3168
  font-weight: 600;
3169
+ line-height: 1;
1903
3170
  text-transform: uppercase;
1904
3171
  letter-spacing: 0.05em;
1905
3172
  color: oklch(var(--nc) / 0.6);
1906
3173
  user-select: none;
1907
3174
  }
1908
3175
 
3176
+ .hai-markdown :global(.hai-md-code-view-switch) {
3177
+ display: inline-flex;
3178
+ align-items: center;
3179
+ gap: 0.16rem;
3180
+ padding: 0.15rem;
3181
+ border-radius: 9999px;
3182
+ border: 1px solid oklch(var(--nc) / 0.16);
3183
+ background: oklch(var(--nc) / 0.08);
3184
+ flex-shrink: 0;
3185
+ }
3186
+
3187
+ .hai-markdown :global(.hai-md-code-view-btn) {
3188
+ min-height: 1.7rem;
3189
+ padding: 0 0.72rem;
3190
+ border: none;
3191
+ border-radius: 9999px;
3192
+ background: transparent;
3193
+ color: oklch(var(--nc) / 0.72);
3194
+ font-size: 0.86rem;
3195
+ font-weight: 600;
3196
+ line-height: 1;
3197
+ cursor: pointer;
3198
+ transition:
3199
+ background-color 0.15s ease,
3200
+ color 0.15s ease;
3201
+ }
3202
+
3203
+ .hai-markdown :global(.hai-md-code-view-btn[aria-pressed='true']) {
3204
+ background: oklch(var(--nc) / 0.2);
3205
+ color: oklch(var(--nc));
3206
+ }
3207
+
3208
+ .hai-markdown :global(.hai-md-code-preview-hint) {
3209
+ font-size: 0.75rem;
3210
+ line-height: 1;
3211
+ color: oklch(var(--nc) / 0.48);
3212
+ white-space: nowrap;
3213
+ overflow: hidden;
3214
+ text-overflow: ellipsis;
3215
+ }
3216
+
1909
3217
  .hai-markdown :global(.hai-md-code-lang-empty) {
1910
3218
  opacity: 0.7;
1911
3219
  }
@@ -1945,8 +3253,21 @@ ${safeCode}
1945
3253
  ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
1946
3254
  'Liberation Mono', monospace;
1947
3255
  font-size: 0.875rem;
1948
- line-height: 1.6;
3256
+ line-height: 1.45;
1949
3257
  tab-size: 2;
3258
+
3259
+ /* Shiki CSS Variables 主题(由 highlight.ts 的 createCssVariablesTheme 引用) */
3260
+ --hai-hl-foreground: oklch(var(--nc));
3261
+ --hai-hl-background: transparent;
3262
+ --hai-hl-token-keyword: oklch(0.7 0.15 280);
3263
+ --hai-hl-token-string: oklch(0.75 0.12 150);
3264
+ --hai-hl-token-string-expression: oklch(0.75 0.12 150);
3265
+ --hai-hl-token-constant: oklch(0.78 0.12 70);
3266
+ --hai-hl-token-comment: oklch(var(--nc) / 0.45);
3267
+ --hai-hl-token-function: oklch(0.72 0.14 220);
3268
+ --hai-hl-token-parameter: oklch(0.72 0.12 35);
3269
+ --hai-hl-token-punctuation: oklch(var(--nc) / 0.7);
3270
+ --hai-hl-token-link: oklch(0.7 0.15 280);
1950
3271
  }
1951
3272
 
1952
3273
  .hai-markdown :global(.hai-md-code-preview-slot) {
@@ -1954,6 +3275,76 @@ ${safeCode}
1954
3275
  background: oklch(var(--b1));
1955
3276
  }
1956
3277
 
3278
+ .hai-markdown :global(.hai-md-code-block[data-code-view='code'] .hai-md-code-preview-slot) {
3279
+ display: none;
3280
+ }
3281
+
3282
+ .hai-markdown :global(.hai-md-code-block[data-code-view='preview'] pre) {
3283
+ display: none;
3284
+ }
3285
+
3286
+ .hai-markdown :global(.hai-md-code-block[data-code-view='preview'] .hai-md-code-preview-slot) {
3287
+ display: block;
3288
+ }
3289
+
3290
+ .hai-markdown-code-only :global(.hai-md-code-block) {
3291
+ width: 100% !important;
3292
+ max-width: none !important;
3293
+ min-width: 0;
3294
+ margin: 0;
3295
+ height: auto;
3296
+ min-height: 0;
3297
+ flex: 1 1 auto;
3298
+ display: flex !important;
3299
+ align-self: stretch;
3300
+ align-items: stretch;
3301
+ flex-direction: column;
3302
+ overflow: hidden;
3303
+ }
3304
+
3305
+ .hai-markdown-code-only :global(.hai-md-code-header),
3306
+ .hai-markdown-code-only :global(.hai-md-code-block pre),
3307
+ .hai-markdown-code-only :global(.hai-md-code-preview-slot) {
3308
+ width: 100%;
3309
+ min-width: 0;
3310
+ min-height: 0;
3311
+ box-sizing: border-box;
3312
+ }
3313
+
3314
+ /* 代码/预览切换时统一固定 header 高度,避免视觉抖动。 */
3315
+ .hai-markdown-code-only :global(.hai-md-code-header) {
3316
+ flex: 0 0 3.1rem;
3317
+ height: 3.1rem;
3318
+ min-height: 3.1rem;
3319
+ padding: 0 1rem;
3320
+ align-items: center;
3321
+ }
3322
+
3323
+ .hai-markdown-code-only :global(.hai-md-code-block pre),
3324
+ .hai-markdown-code-only :global(.hai-md-code-preview-slot) {
3325
+ flex: 1 1 auto;
3326
+ height: auto;
3327
+ min-height: 0;
3328
+ }
3329
+
3330
+ .hai-markdown-code-only :global(.hai-md-code-block pre) {
3331
+ overflow: auto;
3332
+ }
3333
+
3334
+ .hai-markdown-code-only :global(.hai-md-code-preview-slot) {
3335
+ background: oklch(var(--n));
3336
+ color: oklch(var(--nc));
3337
+ }
3338
+
3339
+ .hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview']) {
3340
+ flex: 1 1 auto;
3341
+ }
3342
+
3343
+ .hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview'] .hai-md-code-preview-slot) {
3344
+ flex: 1 1 auto;
3345
+ display: flex;
3346
+ }
3347
+
1957
3348
  .hai-markdown :global(blockquote) {
1958
3349
  margin: 1em 0;
1959
3350
  padding: 0.65em 1.1em;
@@ -2071,14 +3462,29 @@ ${safeCode}
2071
3462
  box-shadow: inset 0 -1px 0 oklch(var(--bc) / 0.1);
2072
3463
  }
2073
3464
 
2074
- .hai-md-preview-card {
3465
+ :global(.hai-md-preview-card) {
2075
3466
  margin: 0;
2076
3467
  padding: 1rem;
2077
3468
  background: oklch(var(--b1));
2078
3469
  color: oklch(var(--bc));
2079
3470
  }
2080
3471
 
2081
- .hai-md-preview-head {
3472
+ .hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview'] .hai-md-preview-card) {
3473
+ display: flex;
3474
+ flex-direction: column;
3475
+ flex: 1 1 auto;
3476
+ min-height: 0;
3477
+ padding: 0.95rem 1rem;
3478
+ background: oklch(var(--n));
3479
+ color: oklch(var(--nc));
3480
+ }
3481
+
3482
+ .hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview'] .hai-md-preview-head),
3483
+ .hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview'] .hai-md-preview-desc) {
3484
+ display: none;
3485
+ }
3486
+
3487
+ :global(.hai-md-preview-head) {
2082
3488
  font-size: 0.8rem;
2083
3489
  font-weight: 700;
2084
3490
  letter-spacing: 0.04em;
@@ -2086,12 +3492,43 @@ ${safeCode}
2086
3492
  color: oklch(var(--bc) / 0.7);
2087
3493
  }
2088
3494
 
2089
- .hai-md-preview-desc {
3495
+ :global(.hai-md-preview-desc) {
2090
3496
  margin: 0.5rem 0 0;
2091
3497
  font-size: 0.875rem;
2092
3498
  color: oklch(var(--bc) / 0.7);
2093
3499
  }
2094
3500
 
3501
+ :global(.hai-md-preview-empty) {
3502
+ min-height: 100%;
3503
+ display: flex;
3504
+ flex-direction: column;
3505
+ gap: 0.62rem;
3506
+ }
3507
+
3508
+ :global(.hai-md-preview-empty-title) {
3509
+ margin: 0;
3510
+ font-size: 0.9rem;
3511
+ font-weight: 700;
3512
+ letter-spacing: 0.01em;
3513
+ color: oklch(var(--bc) / 0.85);
3514
+ }
3515
+
3516
+ :global(.hai-md-preview-empty-text) {
3517
+ margin: 0;
3518
+ font-size: 0.84rem;
3519
+ line-height: 1.58;
3520
+ white-space: pre-wrap;
3521
+ color: oklch(var(--bc) / 0.74);
3522
+ }
3523
+
3524
+ .hai-markdown-code-only :global(.hai-md-preview-empty-title) {
3525
+ color: oklch(var(--nc) / 0.88);
3526
+ }
3527
+
3528
+ .hai-markdown-code-only :global(.hai-md-preview-empty-text) {
3529
+ color: oklch(var(--nc) / 0.72);
3530
+ }
3531
+
2095
3532
  :global(.hai-md-preview-card pre) {
2096
3533
  margin: 0.85rem 0 0;
2097
3534
  padding: 0.85rem 1rem;
@@ -2105,7 +3542,7 @@ ${safeCode}
2105
3542
  color: oklch(var(--er));
2106
3543
  }
2107
3544
 
2108
- .hai-md-preview-frame {
3545
+ :global(.hai-md-preview-frame) {
2109
3546
  display: block;
2110
3547
  width: 100%;
2111
3548
  min-height: 280px;
@@ -2115,10 +3552,19 @@ ${safeCode}
2115
3552
  background: white;
2116
3553
  }
2117
3554
 
2118
- .hai-md-preview-loading {
3555
+ .hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview'] .hai-md-preview-frame) {
3556
+ flex: 1 1 auto;
3557
+ height: 100%;
3558
+ min-height: 0;
3559
+ margin-top: 0;
3560
+ border: none;
3561
+ border-radius: 0;
3562
+ }
3563
+
3564
+ :global(.hai-md-preview-loading) {
2119
3565
  width: 100%;
2120
3566
  height: 4px;
2121
- margin-top: 0.85rem;
3567
+ margin-top: 0.5rem;
2122
3568
  border-radius: 9999px;
2123
3569
  background: linear-gradient(
2124
3570
  90deg,
@@ -2130,27 +3576,10 @@ ${safeCode}
2130
3576
  animation: hai-md-loading 1.2s linear infinite;
2131
3577
  }
2132
3578
 
2133
- .hai-md-preview-rendered :global(:first-child) {
3579
+ :global(.hai-md-preview-rendered > :first-child) {
2134
3580
  margin-top: 0.85rem;
2135
3581
  }
2136
3582
 
2137
- /* Shiki CSS Variables 主题 — 代码高亮 token 颜色 */
2138
- .hai-markdown :global(.hai-md-code-block code) {
2139
- --hai-hl-fg: oklch(var(--nc));
2140
- --hai-hl-bg: transparent;
2141
-
2142
- /* token 颜色 */
2143
- --hai-hl-token-keyword: oklch(0.7 0.15 280);
2144
- --hai-hl-token-string: oklch(0.75 0.12 150);
2145
- --hai-hl-token-string-expression: oklch(0.75 0.12 150);
2146
- --hai-hl-token-constant: oklch(0.78 0.12 70);
2147
- --hai-hl-token-comment: oklch(var(--nc) / 0.45);
2148
- --hai-hl-token-function: oklch(0.72 0.14 220);
2149
- --hai-hl-token-parameter: oklch(0.72 0.12 35);
2150
- --hai-hl-token-punctuation: oklch(var(--nc) / 0.7);
2151
- --hai-hl-token-link: oklch(0.7 0.15 280);
2152
- }
2153
-
2154
3583
  @keyframes hai-md-loading {
2155
3584
  from {
2156
3585
  background-position: 220px 0;
@@ -2161,16 +3590,31 @@ ${safeCode}
2161
3590
  }
2162
3591
  }
2163
3592
 
2164
- @media (max-width: 960px) {
2165
- .hai-ai-doc-layout,
2166
- .hai-ai-doc-layout.hai-ai-doc-layout-collapsed {
2167
- grid-template-columns: minmax(0, 1fr);
3593
+ @container ai-doc-editor (max-width: 800px) {
3594
+ .hai-ai-doc-layout {
3595
+ display: flex;
3596
+ min-height: 0;
2168
3597
  }
2169
3598
 
2170
3599
  .hai-ai-doc-outline {
2171
- display: none;
3600
+ position: absolute;
3601
+ top: 0.65rem;
3602
+ left: 0.65rem;
3603
+ bottom: 0.85rem;
3604
+ width: min(220px, calc(100% - 1.3rem));
3605
+ max-width: 220px;
3606
+ z-index: 14;
3607
+ padding: 0.5rem 0.625rem 0.9rem 0.75rem;
3608
+ border: 1px solid oklch(var(--bc) / 0.12);
3609
+ border-radius: 0.95rem;
3610
+ background:
3611
+ linear-gradient(180deg, oklch(var(--b2) / 0.84), oklch(var(--b1) / 0.92));
3612
+ box-shadow: 0 20px 40px -28px oklch(var(--bc) / 0.35);
3613
+ backdrop-filter: blur(8px);
2172
3614
  }
3615
+ }
2173
3616
 
3617
+ @media (max-width: 960px) {
2174
3618
  .hai-ai-doc-topbar {
2175
3619
  padding-left: 1rem;
2176
3620
  padding-right: 1rem;
@@ -2203,7 +3647,7 @@ ${safeCode}
2203
3647
 
2204
3648
  .hai-ai-doc-selection-toolbar {
2205
3649
  max-width: calc(100vw - 2rem);
2206
- overflow: auto;
3650
+ overflow: hidden;
2207
3651
  }
2208
3652
  }
2209
3653
  </style>