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

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,6 +50,129 @@
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>`
@@ -123,8 +249,16 @@
123
249
  oncopydocument,
124
250
  // 块级格式动作回调。
125
251
  onapplyblockformat,
252
+ // 细粒度块样式动作回调。
253
+ onapplyblockstyle,
126
254
  // 行内格式动作回调。
127
255
  onapplyinlineformat,
256
+ // 对齐动作回调。
257
+ onapplyalignment,
258
+ // 链接设置动作回调。
259
+ onapplylink,
260
+ // 颜色设置动作回调。
261
+ onapplycolor,
128
262
  // 选区复制回调。
129
263
  oncopyselection,
130
264
  // 选区注释回调。
@@ -141,10 +275,10 @@
141
275
  let selectedText = $state('')
142
276
  // 选区工具条显示开关。
143
277
  let selectionToolbarVisible = $state(false)
144
- // 改写菜单显示开关。
145
- let rewriteMenuOpen = $state(false)
278
+ // 选区工具条当前展开的子菜单,统一管理避免多个面板互相覆盖。
279
+ let activeSelectionMenu = $state<SelectionMenuKind | null>(null)
146
280
  // 文档头部标题只有在正文首个标题滚出可视区后才显示,避免双标题并排出现。
147
- let showPinnedTitle = $state(Boolean(title))
281
+ let showPinnedTitle = $state(false)
148
282
  // 选区工具条在滚动容器中的定位坐标。
149
283
  let toolbarPosition = $state<SelectionToolbarPosition>({
150
284
  top: 0,
@@ -152,12 +286,33 @@
152
286
  placement: 'top',
153
287
  alignment: 'center',
154
288
  })
289
+ // 选区工具条自身 DOM,用于根据真实尺寸修正贴边位置。
290
+ let selectionToolbarEl = $state<HTMLDivElement | null>(null)
291
+ // 当前展开菜单对应的触发按钮,用来把面板锚定到具体按钮下方/上方。
292
+ let activeSelectionMenuTrigger = $state<HTMLElement | null>(null)
293
+ // 菜单面板相对工具条的水平锚点。
294
+ let selectionMenuLeft = $state(0)
295
+ // 菜单面板在锚点处的对齐方式,避免宽面板越过正文边界。
296
+ let selectionMenuAlignment = $state<'left' | 'center' | 'right'>('center')
155
297
  // documentCopied 只负责顶部复制按钮的瞬时反馈,不和正文内容状态混用。
156
298
  let documentCopied = $state(false)
157
299
  // copyFeedbackTimer 用来保证连续点击复制时,成功态能按最后一次操作重新计时。
158
- let copyFeedbackTimer: ReturnType<typeof window.setTimeout> | undefined = $state()
300
+ let copyFeedbackTimer: number | undefined = $state()
159
301
  // 每个代码块的运行状态和预览结果,key 为 codeBlockId。
160
302
  let codePreviews = $state<Record<string, CodePreviewState>>({})
303
+ // selectionFormatState 跟随真实 DOM 选区,负责驱动按钮按下态与下拉默认值。
304
+ let selectionFormatState = $state<SelectionFormatState>({
305
+ ...DEFAULT_SELECTION_FORMAT_STATE,
306
+ })
307
+ // linkDraft 只在链接弹层里暂存输入值,避免每次打开都丢掉正在编辑的链接。
308
+ let linkDraft = $state('')
309
+ // 真实尺寸修正只需要排队一帧,避免反复触发 handleSelectionChange 形成抖动。
310
+ let selectionToolbarMeasurePending = false
311
+ // 选区刷新统一合并到同一帧,避免 selectionchange / mouseup / blur 连续触发时抖动。
312
+ let selectionRefreshFrame: number | undefined
313
+ let selectionRefreshNeedsRemeasure = false
314
+ // 浏览器在滚动到底部或切换焦点时会短暂给出空选区,这里延迟一拍再关闭工具条。
315
+ let selectionToolbarCloseTimer: number | undefined
161
316
 
162
317
  // code 类型产物通常只有裸代码文本,这里统一包成 fenced block 进入同一条渲染链路。
163
318
  const documentContent = $derived(
@@ -182,16 +337,35 @@
182
337
  const resolvedRewriteActions = $derived(
183
338
  resolveRewriteActions(rewriteActions, onrewrite),
184
339
  )
340
+ // 新版工具条支持 paragraph / heading1-4,优先走细粒度回调,避免把旧接口误当成新接口调用。
341
+ const richBlockFormattingEnabled = $derived(Boolean(onapplyblockstyle))
342
+ // 旧版 heading / bullet 回调仍然保留,用于兼容外部还没迁移的调用方。
343
+ const legacyBlockFormattingEnabled = $derived(
344
+ Boolean(onapplyblockformat) && !onapplyblockstyle,
345
+ )
346
+ // 这里统一判断是否存在任意块级格式能力,方便控制分隔线和浮层显隐。
347
+ const anyBlockFormattingEnabled = $derived(
348
+ richBlockFormattingEnabled || legacyBlockFormattingEnabled,
349
+ )
185
350
  // selectionToolsEnabled 统一判断选区工具条是否值得出现,避免正文只读展示时还露出空浮层。
186
351
  const selectionToolsEnabled = $derived(
187
352
  [
188
353
  onrewrite,
189
- onapplyblockformat,
354
+ anyBlockFormattingEnabled,
190
355
  onapplyinlineformat,
356
+ onapplyalignment,
357
+ onapplylink,
358
+ onapplycolor,
191
359
  oncopyselection,
192
360
  onannotation,
193
361
  ].some(Boolean),
194
362
  )
363
+ // 当前段落样式的展示文案,直接来自 `selectionFormatState`,避免按钮标题和下拉选中值不同步。
364
+ const activeBlockFormatOption = $derived(
365
+ BLOCK_FORMAT_OPTIONS.find(
366
+ option => option.value === selectionFormatState.blockFormat,
367
+ ) ?? BLOCK_FORMAT_OPTIONS[0],
368
+ )
195
369
  // readerDocumentClass 只负责正文文章区域,不和外层容器类名混用。
196
370
  const readerDocumentClass = $derived(
197
371
  cn(
@@ -259,6 +433,33 @@
259
433
  if (copyFeedbackTimer) {
260
434
  window.clearTimeout(copyFeedbackTimer)
261
435
  }
436
+
437
+ if (selectionRefreshFrame) {
438
+ window.cancelAnimationFrame(selectionRefreshFrame)
439
+ }
440
+
441
+ if (selectionToolbarCloseTimer) {
442
+ window.clearTimeout(selectionToolbarCloseTimer)
443
+ }
444
+ }
445
+ })
446
+
447
+ $effect(() => {
448
+ if (typeof document === 'undefined' || !previewHost || !selectionToolsEnabled) {
449
+ return
450
+ }
451
+
452
+ const handleDocumentSelectionChange = (): void => {
453
+ queueSelectionRefresh()
454
+ }
455
+
456
+ document.addEventListener('selectionchange', handleDocumentSelectionChange)
457
+
458
+ return () => {
459
+ document.removeEventListener(
460
+ 'selectionchange',
461
+ handleDocumentSelectionChange,
462
+ )
262
463
  }
263
464
  })
264
465
 
@@ -605,7 +806,6 @@ ${safeCode}
605
806
  const markdownHtml = parseMarkdown(result.content, {
606
807
  enableHighlight,
607
808
  showCopyButton: false,
608
- showRunButton: false,
609
809
  breaks,
610
810
  })
611
811
 
@@ -629,10 +829,323 @@ ${safeCode}
629
829
  return escapePreviewText(value).replace(/\n/g, '&#10;')
630
830
  }
631
831
 
832
+ function getSelectionAnchorElement(selection: Selection): HTMLElement | null {
833
+ const anchorNode = selection.anchorNode
834
+ if (!anchorNode) {
835
+ return null
836
+ }
837
+
838
+ return anchorNode instanceof HTMLElement
839
+ ? anchorNode
840
+ : anchorNode.parentElement
841
+ }
842
+
843
+ function getClosestBlockElement(element: HTMLElement | null): HTMLElement | null {
844
+ if (!element || !previewHost) {
845
+ return null
846
+ }
847
+
848
+ const block = element.closest('p, h1, h2, h3, h4, h5, h6, li')
849
+ return block instanceof HTMLElement && previewHost.contains(block)
850
+ ? block
851
+ : null
852
+ }
853
+
854
+ function readComputedInlineState(element: HTMLElement | null): Pick<
855
+ SelectionFormatState,
856
+ 'bold' | 'italic' | 'strike' | 'underline'
857
+ > {
858
+ if (!element || typeof window === 'undefined') {
859
+ return {
860
+ bold: false,
861
+ italic: false,
862
+ strike: false,
863
+ underline: false,
864
+ }
865
+ }
866
+
867
+ const style = window.getComputedStyle(element)
868
+ const fontWeight = Number.parseInt(style.fontWeight, 10)
869
+ const decoration = `${style.textDecorationLine} ${style.textDecoration}`
870
+ .toLowerCase()
871
+
872
+ return {
873
+ bold: Number.isNaN(fontWeight)
874
+ ? ['bold', 'bolder'].includes(style.fontWeight.toLowerCase())
875
+ : fontWeight >= 600,
876
+ italic:
877
+ style.fontStyle === 'italic' || style.fontStyle.startsWith('oblique'),
878
+ strike: decoration.includes('line-through'),
879
+ underline: decoration.includes('underline'),
880
+ }
881
+ }
882
+
883
+ function readBlockFormat(element: HTMLElement | null): MarkdownBlockStyleKind {
884
+ const tagName = getClosestBlockElement(element)?.tagName.toLowerCase()
885
+
886
+ if (tagName === 'h1') {
887
+ return 'heading1'
888
+ }
889
+ if (tagName === 'h2') {
890
+ return 'heading2'
891
+ }
892
+ if (tagName === 'h3') {
893
+ return 'heading3'
894
+ }
895
+ if (tagName === 'h4') {
896
+ return 'heading4'
897
+ }
898
+
899
+ return 'paragraph'
900
+ }
901
+
902
+ function normalizeTextAlign(value: string | null | undefined): MarkdownTextAlignKind | null {
903
+ const normalized = value?.trim().toLowerCase()
904
+ if (!normalized) {
905
+ return null
906
+ }
907
+
908
+ if (normalized === 'start') {
909
+ return 'left'
910
+ }
911
+ if (normalized === 'end') {
912
+ return 'right'
913
+ }
914
+
915
+ return normalized === 'center'
916
+ || normalized === 'right'
917
+ || normalized === 'justify'
918
+ || normalized === 'left'
919
+ ? normalized
920
+ : null
921
+ }
922
+
923
+ function readAlignment(element: HTMLElement | null): MarkdownTextAlignKind {
924
+ const block = getClosestBlockElement(element)
925
+ if (!block || typeof window === 'undefined') {
926
+ return 'left'
927
+ }
928
+
929
+ // 对齐样式可能挂在外层 `<hai-align>` 容器上,单读 block 的 inline style 会丢失真实状态。
930
+ const alignHost = block.closest<HTMLElement>('.hai-md-align-block') || block
931
+ const computedAlign = normalizeTextAlign(
932
+ window.getComputedStyle(alignHost).textAlign,
933
+ )
934
+ const attributeAlignCandidate
935
+ = alignHost.dataset.haiAlign
936
+ || alignHost.getAttribute('align')
937
+ || block.getAttribute('align')
938
+ const attributeAlign = normalizeTextAlign(
939
+ attributeAlignCandidate,
940
+ )
941
+
942
+ return computedAlign || attributeAlign || 'left'
943
+ }
944
+
945
+ function readSelectionFormatState(selection: Selection): SelectionFormatState {
946
+ const anchorElement = getSelectionAnchorElement(selection)
947
+ const colorHost = anchorElement?.closest<HTMLElement>('[data-hai-color], [data-hai-bg]')
948
+ const linkHost = anchorElement?.closest<HTMLAnchorElement>('a[href]')
949
+ const codeHost = anchorElement?.closest<HTMLElement>('code')
950
+ const highlightHost = anchorElement?.closest<HTMLElement>('mark')
951
+ const inlineState = readComputedInlineState(anchorElement)
952
+ const hasHighlight = Boolean((highlightHost && previewHost?.contains(highlightHost)) || colorHost?.dataset.haiBg?.trim())
953
+ const textColor = colorHost?.dataset.haiColor?.trim()
954
+ || anchorElement?.closest<HTMLElement>('[data-hai-color]')?.dataset.haiColor
955
+ || null
956
+ const backgroundColor = colorHost?.dataset.haiBg?.trim()
957
+ || anchorElement?.closest<HTMLElement>('[data-hai-bg]')?.dataset.haiBg
958
+ || null
959
+
960
+ return {
961
+ blockFormat: readBlockFormat(anchorElement),
962
+ alignment: readAlignment(anchorElement),
963
+ bold: inlineState.bold,
964
+ strike: inlineState.strike,
965
+ italic: inlineState.italic,
966
+ underline: inlineState.underline,
967
+ highlight: hasHighlight,
968
+ code: Boolean(codeHost && previewHost?.contains(codeHost)),
969
+ linkHref:
970
+ linkHost && previewHost?.contains(linkHost)
971
+ ? (linkHost.getAttribute('href') ?? '')
972
+ : '',
973
+ textColor,
974
+ backgroundColor,
975
+ }
976
+ }
977
+
978
+ function readPreviewSelectionState(): PreviewSelectionState | null {
979
+ if (typeof window === 'undefined' || !previewHost) {
980
+ return null
981
+ }
982
+
983
+ const selection = window.getSelection()
984
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
985
+ return null
986
+ }
987
+
988
+ const anchorNode = selection.anchorNode
989
+ const focusNode = selection.focusNode
990
+ if (
991
+ !anchorNode
992
+ || !focusNode
993
+ || !previewHost.contains(anchorNode)
994
+ || !previewHost.contains(focusNode)
995
+ ) {
996
+ return null
997
+ }
998
+
999
+ const text = selection.toString().trim()
1000
+ if (!text) {
1001
+ return null
1002
+ }
1003
+
1004
+ return {
1005
+ selection,
1006
+ range: selection.getRangeAt(0),
1007
+ text,
1008
+ }
1009
+ }
1010
+
1011
+ function clearPendingSelectionToolbarClose(): void {
1012
+ if (typeof window === 'undefined' || !selectionToolbarCloseTimer) {
1013
+ return
1014
+ }
1015
+
1016
+ window.clearTimeout(selectionToolbarCloseTimer)
1017
+ selectionToolbarCloseTimer = undefined
1018
+ }
1019
+
1020
+ function scheduleSelectionToolbarClose(): void {
1021
+ if (typeof window === 'undefined') {
1022
+ closeSelectionToolbar()
1023
+ return
1024
+ }
1025
+
1026
+ if (!selectionToolbarVisible) {
1027
+ closeSelectionToolbar()
1028
+ return
1029
+ }
1030
+
1031
+ clearPendingSelectionToolbarClose()
1032
+ selectionToolbarCloseTimer = window.setTimeout(() => {
1033
+ selectionToolbarCloseTimer = undefined
1034
+ if (!readPreviewSelectionState()) {
1035
+ closeSelectionToolbar()
1036
+ }
1037
+ }, 120)
1038
+ }
1039
+
1040
+ function queueSelectionRefresh(remeasured = false): void {
1041
+ if (typeof window === 'undefined') {
1042
+ return
1043
+ }
1044
+
1045
+ selectionRefreshNeedsRemeasure ||= remeasured
1046
+ if (selectionRefreshFrame) {
1047
+ return
1048
+ }
1049
+
1050
+ selectionRefreshFrame = window.requestAnimationFrame(() => {
1051
+ const nextRemeasured = selectionRefreshNeedsRemeasure
1052
+ selectionRefreshFrame = undefined
1053
+ selectionRefreshNeedsRemeasure = false
1054
+ handleSelectionChange(nextRemeasured)
1055
+ })
1056
+ }
1057
+
1058
+ function estimateSelectionMenuWidth(menu: SelectionMenuKind): number {
1059
+ if (menu === 'color' || menu === 'link') {
1060
+ return 256
1061
+ }
1062
+
1063
+ if (menu === 'rewrite') {
1064
+ return 320
1065
+ }
1066
+
1067
+ return 288
1068
+ }
1069
+
1070
+ function updateSelectionMenuAnchor(menu: SelectionMenuKind): void {
1071
+ if (!selectionToolbarEl || !activeSelectionMenuTrigger) {
1072
+ selectionMenuLeft = 0
1073
+ selectionMenuAlignment = 'center'
1074
+ return
1075
+ }
1076
+
1077
+ const toolbarWidth = Math.max(
1078
+ selectionToolbarEl.offsetWidth,
1079
+ selectionToolbarEl.clientWidth,
1080
+ )
1081
+ const triggerCenter
1082
+ = activeSelectionMenuTrigger.offsetLeft
1083
+ + activeSelectionMenuTrigger.offsetWidth / 2
1084
+ const estimatedHalfWidth = Math.min(
1085
+ estimateSelectionMenuWidth(menu) / 2,
1086
+ Math.max(112, (toolbarWidth - 16) / 2),
1087
+ )
1088
+ const minCenter = 8 + estimatedHalfWidth
1089
+ const maxCenter = Math.max(minCenter, toolbarWidth - 8 - estimatedHalfWidth)
1090
+
1091
+ if (triggerCenter <= minCenter) {
1092
+ selectionMenuLeft = 0
1093
+ selectionMenuAlignment = 'left'
1094
+ return
1095
+ }
1096
+
1097
+ if (triggerCenter >= maxCenter) {
1098
+ selectionMenuLeft = toolbarWidth
1099
+ selectionMenuAlignment = 'right'
1100
+ return
1101
+ }
1102
+
1103
+ selectionMenuLeft = triggerCenter
1104
+ selectionMenuAlignment = 'center'
1105
+ }
1106
+
1107
+ function scheduleSelectionToolbarMeasurement(): void {
1108
+ if (
1109
+ typeof window === 'undefined'
1110
+ || selectionToolbarMeasurePending
1111
+ || !selectionToolbarVisible
1112
+ ) {
1113
+ return
1114
+ }
1115
+
1116
+ selectionToolbarMeasurePending = true
1117
+ window.requestAnimationFrame(() => {
1118
+ selectionToolbarMeasurePending = false
1119
+ if (selectionToolbarVisible) {
1120
+ handleSelectionChange(true)
1121
+ }
1122
+ })
1123
+ }
1124
+
1125
+ function toggleSelectionMenu(menu: SelectionMenuKind, event?: MouseEvent): void {
1126
+ const opening = activeSelectionMenu !== menu
1127
+ activeSelectionMenu = opening ? menu : null
1128
+ activeSelectionMenuTrigger = opening && event?.currentTarget instanceof HTMLElement
1129
+ ? event.currentTarget
1130
+ : null
1131
+ if (opening) {
1132
+ updateSelectionMenuAnchor(menu)
1133
+ }
1134
+ if (opening && menu === 'link') {
1135
+ linkDraft = selectionFormatState.linkHref || 'https://'
1136
+ }
1137
+ }
1138
+
632
1139
  function closeSelectionToolbar(): void {
1140
+ clearPendingSelectionToolbarClose()
633
1141
  selectionToolbarVisible = false
634
- rewriteMenuOpen = false
1142
+ activeSelectionMenu = null
1143
+ activeSelectionMenuTrigger = null
1144
+ selectionMenuLeft = 0
1145
+ selectionMenuAlignment = 'center'
635
1146
  selectedText = ''
1147
+ linkDraft = ''
1148
+ selectionFormatState = { ...DEFAULT_SELECTION_FORMAT_STATE }
636
1149
  }
637
1150
 
638
1151
  /**
@@ -683,9 +1196,8 @@ ${safeCode}
683
1196
  return
684
1197
  }
685
1198
 
686
- const firstHeading = previewHost.querySelector<HTMLElement>(
687
- '[data-heading-id]',
688
- )
1199
+ const firstHeading
1200
+ = previewHost.querySelector<HTMLElement>('[data-heading-id]')
689
1201
  if (!firstHeading) {
690
1202
  showPinnedTitle = true
691
1203
  return
@@ -700,7 +1212,7 @@ ${safeCode}
700
1212
  syncActiveHeadingFromScroll()
701
1213
  syncPinnedTitleVisibility()
702
1214
  if (selectionToolbarVisible) {
703
- closeSelectionToolbar()
1215
+ queueSelectionRefresh(true)
704
1216
  }
705
1217
  ondocumentscroll?.(event)
706
1218
  }
@@ -724,7 +1236,7 @@ ${safeCode}
724
1236
  * 选区工具条依赖真实 DOM 选区范围,因此只在 selection 落在当前正文容器内时显示。
725
1237
  * 一旦内容刷新或滚动位置变化,就主动关闭,避免把旧选区动作误用到新内容上。
726
1238
  */
727
- function handleSelectionChange(): void {
1239
+ function handleSelectionChange(remeasured = false): void {
728
1240
  if (
729
1241
  !selectionToolsEnabled
730
1242
  || typeof window === 'undefined'
@@ -734,79 +1246,70 @@ ${safeCode}
734
1246
  return
735
1247
  }
736
1248
 
737
- const selection = window.getSelection()
738
- if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
739
- closeSelectionToolbar()
740
- return
741
- }
1249
+ clearPendingSelectionToolbarClose()
742
1250
 
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()
1251
+ const previewSelection = readPreviewSelectionState()
1252
+ if (!previewSelection) {
1253
+ scheduleSelectionToolbarClose()
752
1254
  return
753
1255
  }
754
1256
 
755
- const text = selection.toString().trim()
756
- if (!text) {
757
- closeSelectionToolbar()
758
- return
759
- }
760
-
761
- const range = selection.getRangeAt(0)
1257
+ const { selection, range, text } = previewSelection
762
1258
  const rect = range.getBoundingClientRect()
763
1259
  const hostRect = editorScrollHost.getBoundingClientRect()
764
1260
  // 浮层尺寸在首次渲染前拿不到精确高度,这里用稳定的经验值决定上下避让方向,避免直接盖住选中文本。
765
- const estimatedToolbarHeight = rewriteMenuOpen ? 132 : 56
1261
+ const estimatedToolbarHeight = 60
766
1262
  const offset = 12
767
1263
  const topSpace = rect.top - hostRect.top
768
1264
  const bottomSpace = hostRect.bottom - rect.bottom
769
- const placement
770
- = topSpace >= estimatedToolbarHeight + offset || bottomSpace < estimatedToolbarHeight
1265
+ const placement: SelectionToolbarPosition['placement']
1266
+ = topSpace >= estimatedToolbarHeight + offset
1267
+ || bottomSpace < estimatedToolbarHeight
771
1268
  ? 'top'
772
1269
  : 'bottom'
773
1270
  const center = rect.left - hostRect.left + rect.width / 2
774
1271
  const horizontalPadding = 16
1272
+ const measuredToolbarHalfWidth = selectionToolbarEl
1273
+ ? selectionToolbarEl.offsetWidth / 2
1274
+ : 320
775
1275
  const estimatedToolbarHalfWidth = Math.min(
776
- 272,
777
- Math.max(
778
- 132,
779
- (editorScrollHost.clientWidth - horizontalPadding * 2) / 2,
780
- ),
1276
+ Math.max(132, measuredToolbarHalfWidth),
1277
+ Math.max(132, (editorScrollHost.clientWidth - horizontalPadding * 2) / 2),
781
1278
  )
782
1279
  const minCenter = horizontalPadding + estimatedToolbarHalfWidth
783
1280
  const maxCenter = editorScrollHost.clientWidth - minCenter
784
- const alignment
1281
+ const alignment: SelectionToolbarPosition['alignment']
785
1282
  = center <= minCenter
786
1283
  ? 'left'
787
1284
  : center >= maxCenter
788
1285
  ? 'right'
789
1286
  : '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
1287
+ const toolbarLeft = alignment === 'left'
1288
+ ? horizontalPadding
1289
+ : alignment === 'right'
1290
+ ? editorScrollHost.clientWidth - horizontalPadding
1291
+ : center
1292
+ const toolbarTop = placement === 'top'
1293
+ ? editorScrollHost.scrollTop + rect.top - hostRect.top - estimatedToolbarHeight - offset
1294
+ : editorScrollHost.scrollTop + rect.bottom - hostRect.top + offset
800
1295
 
801
1296
  selectedText = text
802
1297
  selectionToolbarVisible = true
803
- rewriteMenuOpen = false
1298
+ selectionFormatState = readSelectionFormatState(selection)
804
1299
  toolbarPosition = {
805
1300
  top: Math.max(12, toolbarTop),
806
1301
  left: Math.max(horizontalPadding, toolbarLeft),
807
1302
  placement,
808
1303
  alignment,
809
1304
  }
1305
+
1306
+ if (activeSelectionMenu) {
1307
+ updateSelectionMenuAnchor(activeSelectionMenu)
1308
+ }
1309
+
1310
+ if (!remeasured) {
1311
+ scheduleSelectionToolbarMeasurement()
1312
+ }
810
1313
  }
811
1314
 
812
1315
  async function applyRewrite(actionId: string): Promise<void> {
@@ -814,7 +1317,8 @@ ${safeCode}
814
1317
  return
815
1318
  }
816
1319
 
817
- rewriteMenuOpen = false
1320
+ activeSelectionMenu = null
1321
+ activeSelectionMenuTrigger = null
818
1322
  await onrewrite({
819
1323
  actionId,
820
1324
  selectedText,
@@ -825,26 +1329,63 @@ ${safeCode}
825
1329
 
826
1330
  function applyBlockFormat(kind: MarkdownBlockFormatKind): void {
827
1331
  onapplyblockformat?.(kind)
828
- closeSelectionToolbar()
1332
+ activeSelectionMenu = null
1333
+ activeSelectionMenuTrigger = null
1334
+ queueSelectionRefresh()
1335
+ }
1336
+
1337
+ function applyBlockStyle(kind: MarkdownBlockStyleKind): void {
1338
+ onapplyblockstyle?.(kind)
1339
+ activeSelectionMenu = null
1340
+ activeSelectionMenuTrigger = null
1341
+ queueSelectionRefresh()
829
1342
  }
830
1343
 
831
1344
  function applyInlineFormat(kind: MarkdownInlineFormatKind): void {
832
1345
  onapplyinlineformat?.(kind)
833
- closeSelectionToolbar()
1346
+ queueSelectionRefresh()
1347
+ }
1348
+
1349
+ function applyAlignment(kind: MarkdownTextAlignKind): void {
1350
+ onapplyalignment?.(kind)
1351
+ activeSelectionMenu = null
1352
+ activeSelectionMenuTrigger = null
1353
+ queueSelectionRefresh()
1354
+ }
1355
+
1356
+ function applyLink(): void {
1357
+ onapplylink?.(linkDraft.trim() || null)
1358
+ activeSelectionMenu = null
1359
+ activeSelectionMenuTrigger = null
1360
+ queueSelectionRefresh()
1361
+ }
1362
+
1363
+ function removeLink(): void {
1364
+ onapplylink?.(null)
1365
+ activeSelectionMenu = null
1366
+ activeSelectionMenuTrigger = null
1367
+ queueSelectionRefresh()
1368
+ }
1369
+
1370
+ function applyColor(request: MarkdownColorFormatRequest): void {
1371
+ onapplycolor?.(request)
1372
+ queueSelectionRefresh()
834
1373
  }
835
1374
 
836
1375
  async function copySelection(): Promise<void> {
837
1376
  await oncopyselection?.()
838
- closeSelectionToolbar()
1377
+ activeSelectionMenu = null
1378
+ activeSelectionMenuTrigger = null
839
1379
  }
840
1380
 
841
1381
  async function annotateSelection(): Promise<void> {
842
1382
  await onannotation?.()
843
- closeSelectionToolbar()
1383
+ activeSelectionMenu = null
1384
+ activeSelectionMenuTrigger = null
844
1385
  }
845
1386
 
846
1387
  function handlePreviewMouseUp(): void {
847
- handleSelectionChange()
1388
+ queueSelectionRefresh()
848
1389
  onpreviewmouseup?.()
849
1390
  }
850
1391
 
@@ -853,7 +1394,7 @@ ${safeCode}
853
1394
  }
854
1395
 
855
1396
  function handlePreviewBlur(): void {
856
- closeSelectionToolbar()
1397
+ queueSelectionRefresh(true)
857
1398
  onpreviewblur?.()
858
1399
  }
859
1400
  </script>
@@ -893,6 +1434,7 @@ ${safeCode}
893
1434
  type='button'
894
1435
  class='hai-ai-doc-toolbar-icon'
895
1436
  aria-label={uiM('markdown_undo')}
1437
+ title={`${uiM('markdown_undo')} (Ctrl/Cmd+Z)`}
896
1438
  disabled={undoDisabled}
897
1439
  onclick={onundo}
898
1440
  >
@@ -909,6 +1451,7 @@ ${safeCode}
909
1451
  type='button'
910
1452
  class='hai-ai-doc-toolbar-icon'
911
1453
  aria-label={uiM('markdown_redo')}
1454
+ title={`${uiM('markdown_redo')} (Ctrl/Cmd+Shift+Z)`}
912
1455
  disabled={redoDisabled}
913
1456
  onclick={onredo}
914
1457
  >
@@ -951,12 +1494,12 @@ ${safeCode}
951
1494
  </button>
952
1495
 
953
1496
  <AiDocumentDownloadMenu
954
- content={content}
955
- title={title}
956
- sourceKind={sourceKind}
957
- codeLanguage={codeLanguage}
1497
+ {content}
1498
+ {title}
1499
+ {sourceKind}
1500
+ {codeLanguage}
958
1501
  actions={downloadActions}
959
- ondownload={ondownload}
1502
+ {ondownload}
960
1503
  showLabel={true}
961
1504
  iconOnly={false}
962
1505
  triggerTitle={uiM('markdown_download')}
@@ -1046,14 +1589,7 @@ ${safeCode}
1046
1589
  class='hai-ai-doc-scroll'
1047
1590
  onclick={handleClick}
1048
1591
  onscroll={handleDocumentScroll}
1049
- onkeyup={handleSelectionChange}
1050
- onfocusout={() => {
1051
- if (typeof window !== 'undefined') {
1052
- window.setTimeout(() => {
1053
- handleSelectionChange()
1054
- }, 0)
1055
- }
1056
- }}
1592
+ onkeyup={() => handleSelectionChange()}
1057
1593
  >
1058
1594
  {#if selectionToolbarVisible}
1059
1595
  <div
@@ -1063,178 +1599,503 @@ ${safeCode}
1063
1599
  style={`top:${toolbarPosition.top}px; left:${toolbarPosition.left}px;`}
1064
1600
  >
1065
1601
  <div
1066
- class='hai-ai-doc-selection-toolbar'
1067
- role='toolbar'
1068
- tabindex='-1'
1069
- onmousedown={event => event.preventDefault()}
1602
+ class='hai-ai-doc-selection-chrome'
1070
1603
  >
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}
1604
+ <div
1605
+ bind:this={selectionToolbarEl}
1606
+ class='hai-ai-doc-selection-toolbar'
1607
+ role='toolbar'
1608
+ tabindex='-1'
1609
+ onmousedown={event => event.preventDefault()}
1610
+ >
1088
1611
  {#if onrewrite}
1089
- <span class='hai-ai-doc-selection-divider'></span>
1612
+ <button
1613
+ type='button'
1614
+ class={cn(
1615
+ 'hai-ai-doc-selection-chip',
1616
+ activeSelectionMenu === 'rewrite'
1617
+ ? 'hai-ai-doc-selection-chip--active'
1618
+ : '',
1619
+ )}
1620
+ disabled={rewritePending}
1621
+ onclick={event => toggleSelectionMenu('rewrite', event)}
1622
+ >
1623
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1624
+ <path
1625
+ 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'
1626
+ ></path>
1627
+ </svg>
1628
+ <span>{uiM('markdown_rewrite')}</span>
1629
+ </button>
1090
1630
  {/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
1631
 
1117
- {#if onapplyinlineformat}
1118
- {#if onapplyblockformat}
1119
- <span class='hai-ai-doc-selection-divider'></span>
1632
+ {#if richBlockFormattingEnabled}
1633
+ {#if onrewrite}
1634
+ <span class='hai-ai-doc-selection-divider'></span>
1635
+ {/if}
1636
+ <button
1637
+ type='button'
1638
+ class={cn(
1639
+ 'hai-ai-doc-selection-trigger',
1640
+ activeSelectionMenu === 'block'
1641
+ ? 'hai-ai-doc-selection-trigger--active'
1642
+ : '',
1643
+ )}
1644
+ title={uiM('markdown_format_heading')}
1645
+ onclick={event => toggleSelectionMenu('block', event)}
1646
+ >
1647
+ <span class='hai-ai-doc-selection-trigger-label'>
1648
+ {activeBlockFormatOption.shortLabel}
1649
+ </span>
1650
+ <svg
1651
+ viewBox='0 0 24 24'
1652
+ aria-hidden='true'
1653
+ class='hai-ai-doc-selection-trigger-chevron'
1654
+ >
1655
+ <path
1656
+ 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'
1657
+ ></path>
1658
+ </svg>
1659
+ </button>
1660
+ {:else if legacyBlockFormattingEnabled}
1661
+ {#if onrewrite}
1662
+ <span class='hai-ai-doc-selection-divider'></span>
1663
+ {/if}
1664
+ <button
1665
+ type='button'
1666
+ class='hai-ai-doc-selection-btn'
1667
+ title={uiM('markdown_format_heading')}
1668
+ onclick={() => applyBlockFormat('heading')}
1669
+ >
1670
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1671
+ <path
1672
+ 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'
1673
+ ></path>
1674
+ </svg>
1675
+ </button>
1676
+ <button
1677
+ type='button'
1678
+ class='hai-ai-doc-selection-btn'
1679
+ title={uiM('markdown_format_bullet')}
1680
+ onclick={() => applyBlockFormat('bullet')}
1681
+ >
1682
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1683
+ <path
1684
+ 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'
1685
+ ></path>
1686
+ </svg>
1687
+ </button>
1688
+ {/if}
1689
+
1690
+ {#if onapplyalignment}
1691
+ {#if anyBlockFormattingEnabled || onrewrite}
1692
+ <span class='hai-ai-doc-selection-divider'></span>
1693
+ {/if}
1694
+ <button
1695
+ type='button'
1696
+ class={cn(
1697
+ 'hai-ai-doc-selection-trigger',
1698
+ activeSelectionMenu === 'align'
1699
+ ? 'hai-ai-doc-selection-trigger--active'
1700
+ : '',
1701
+ )}
1702
+ title={uiM('markdown_align')}
1703
+ onclick={event => toggleSelectionMenu('align', event)}
1704
+ >
1705
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1706
+ {#if selectionFormatState.alignment === 'center'}
1707
+ <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>
1708
+ {:else if selectionFormatState.alignment === 'right'}
1709
+ <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>
1710
+ {:else if selectionFormatState.alignment === 'justify'}
1711
+ <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>
1712
+ {:else}
1713
+ <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>
1714
+ {/if}
1715
+ </svg>
1716
+ <svg
1717
+ viewBox='0 0 24 24'
1718
+ aria-hidden='true'
1719
+ class='hai-ai-doc-selection-trigger-chevron'
1720
+ >
1721
+ <path
1722
+ 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'
1723
+ ></path>
1724
+ </svg>
1725
+ </button>
1726
+ {/if}
1727
+
1728
+ {#if onapplyinlineformat || onapplylink || onapplycolor}
1729
+ {#if anyBlockFormattingEnabled || onapplyalignment || onrewrite}
1730
+ <span class='hai-ai-doc-selection-divider'></span>
1731
+ {/if}
1120
1732
  {/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
1733
 
1183
- {#if oncopyselection || onannotation}
1184
1734
  {#if onapplyinlineformat}
1185
- <span class='hai-ai-doc-selection-divider'></span>
1735
+ <button
1736
+ type='button'
1737
+ class={cn(
1738
+ 'hai-ai-doc-selection-btn',
1739
+ selectionFormatState.bold
1740
+ ? 'hai-ai-doc-selection-btn--active'
1741
+ : '',
1742
+ )}
1743
+ title={uiM('markdown_format_bold')}
1744
+ onclick={() => applyInlineFormat('bold')}
1745
+ >
1746
+ <strong>B</strong>
1747
+ </button>
1748
+ <button
1749
+ type='button'
1750
+ class={cn(
1751
+ 'hai-ai-doc-selection-btn',
1752
+ selectionFormatState.strike
1753
+ ? 'hai-ai-doc-selection-btn--active'
1754
+ : '',
1755
+ )}
1756
+ title={uiM('markdown_format_strike')}
1757
+ onclick={() => applyInlineFormat('strike')}
1758
+ >
1759
+ S
1760
+ </button>
1761
+ <button
1762
+ type='button'
1763
+ class={cn(
1764
+ 'hai-ai-doc-selection-btn',
1765
+ selectionFormatState.italic
1766
+ ? 'hai-ai-doc-selection-btn--active'
1767
+ : '',
1768
+ )}
1769
+ title={uiM('markdown_format_italic')}
1770
+ onclick={() => applyInlineFormat('italic')}
1771
+ >
1772
+ <em>I</em>
1773
+ </button>
1774
+ <button
1775
+ type='button'
1776
+ class={cn(
1777
+ 'hai-ai-doc-selection-btn',
1778
+ selectionFormatState.underline
1779
+ ? 'hai-ai-doc-selection-btn--active'
1780
+ : '',
1781
+ )}
1782
+ title={uiM('markdown_format_underline')}
1783
+ onclick={() => applyInlineFormat('underline')}
1784
+ >
1785
+ U
1786
+ </button>
1186
1787
  {/if}
1187
- {/if}
1188
1788
 
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}
1789
+ {#if onapplylink || onapplyinlineformat}
1790
+ <button
1791
+ type='button'
1792
+ class={cn(
1793
+ 'hai-ai-doc-selection-btn',
1794
+ selectionFormatState.linkHref
1795
+ ? 'hai-ai-doc-selection-btn--active'
1796
+ : '',
1797
+ activeSelectionMenu === 'link'
1798
+ ? 'hai-ai-doc-selection-btn--active'
1799
+ : '',
1800
+ )}
1801
+ title={uiM('markdown_format_link')}
1802
+ onclick={event =>
1803
+ onapplylink
1804
+ ? toggleSelectionMenu('link', event)
1805
+ : applyInlineFormat('link')}
1806
+ >
1807
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1808
+ <path
1809
+ 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'
1810
+ ></path>
1811
+ </svg>
1812
+ </button>
1813
+ {/if}
1203
1814
 
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>
1815
+ {#if onapplyinlineformat}
1816
+ <button
1817
+ type='button'
1818
+ class={cn(
1819
+ 'hai-ai-doc-selection-btn',
1820
+ selectionFormatState.code
1821
+ ? 'hai-ai-doc-selection-btn--active'
1822
+ : '',
1823
+ )}
1824
+ title={uiM('markdown_format_code')}
1825
+ onclick={() => applyInlineFormat('code')}
1826
+ >
1827
+ &lt;/&gt;
1828
+ </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
+ {/if}
1219
1843
 
1220
- {#if rewriteMenuOpen}
1221
- <div
1222
- class='hai-ai-doc-rewrite-menu'
1223
- role='menu'
1224
- tabindex='-1'
1225
- onmousedown={event => event.preventDefault()}
1226
- >
1227
- {#each resolvedRewriteActions as action}
1844
+ {#if onapplycolor}
1228
1845
  <button
1229
1846
  type='button'
1230
- disabled={rewritePending}
1231
- onclick={() => applyRewrite(action.id)}
1847
+ class={cn(
1848
+ 'hai-ai-doc-selection-trigger',
1849
+ activeSelectionMenu === 'color'
1850
+ ? 'hai-ai-doc-selection-trigger--active'
1851
+ : '',
1852
+ )}
1853
+ style={`--hai-ai-doc-current-color:${selectionFormatState.textColor ?? '#0f172a'};`}
1854
+ title={uiM('markdown_text_color')}
1855
+ onclick={event => toggleSelectionMenu('color', event)}
1232
1856
  >
1233
- {action.label}
1857
+ <span class='hai-ai-doc-selection-highlight-icon'>A</span>
1858
+ <svg
1859
+ viewBox='0 0 24 24'
1860
+ aria-hidden='true'
1861
+ class='hai-ai-doc-selection-trigger-chevron'
1862
+ >
1863
+ <path
1864
+ 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'
1865
+ ></path>
1866
+ </svg>
1234
1867
  </button>
1235
- {/each}
1868
+ {/if}
1869
+
1870
+ {#if oncopyselection || onannotation}
1871
+ {#if onapplyinlineformat || onapplylink || onapplycolor}
1872
+ <span class='hai-ai-doc-selection-divider'></span>
1873
+ {/if}
1874
+ {/if}
1875
+
1876
+ {#if oncopyselection}
1877
+ <button
1878
+ type='button'
1879
+ class='hai-ai-doc-selection-btn hai-ai-doc-selection-btn-wide'
1880
+ title={uiM('markdown_copy_selection')}
1881
+ onclick={copySelection}
1882
+ >
1883
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1884
+ <path
1885
+ 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'
1886
+ ></path>
1887
+ </svg>
1888
+ <span>{uiM('markdown_copy_selection')}</span>
1889
+ </button>
1890
+ {/if}
1891
+
1892
+ {#if onannotation}
1893
+ <button
1894
+ type='button'
1895
+ class='hai-ai-doc-selection-btn hai-ai-doc-selection-btn-wide'
1896
+ title={uiM('markdown_annotation')}
1897
+ onclick={annotateSelection}
1898
+ >
1899
+ <svg viewBox='0 0 24 24' aria-hidden='true'>
1900
+ <path
1901
+ 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'
1902
+ ></path>
1903
+ </svg>
1904
+ </button>
1905
+ {/if}
1236
1906
  </div>
1237
- {/if}
1907
+
1908
+ {#if activeSelectionMenu === 'rewrite' && resolvedRewriteActions.length > 0}
1909
+ <div
1910
+ class='hai-ai-doc-rewrite-menu'
1911
+ data-menu-alignment={selectionMenuAlignment}
1912
+ role='menu'
1913
+ tabindex='-1'
1914
+ style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
1915
+ onmousedown={event => event.preventDefault()}
1916
+ >
1917
+ {#each resolvedRewriteActions as action}
1918
+ <button
1919
+ type='button'
1920
+ class='hai-ai-doc-rewrite-menu-btn'
1921
+ disabled={rewritePending}
1922
+ onclick={() => applyRewrite(action.id)}
1923
+ >
1924
+ {action.label}
1925
+ </button>
1926
+ {/each}
1927
+ </div>
1928
+ {:else if activeSelectionMenu === 'block' && richBlockFormattingEnabled}
1929
+ <div
1930
+ class='hai-ai-doc-selection-panel'
1931
+ data-menu-alignment={selectionMenuAlignment}
1932
+ role='menu'
1933
+ tabindex='-1'
1934
+ style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
1935
+ onmousedown={event => event.preventDefault()}
1936
+ >
1937
+ {#each BLOCK_FORMAT_OPTIONS as option}
1938
+ <button
1939
+ type='button'
1940
+ class={cn(
1941
+ 'hai-ai-doc-selection-menu-btn',
1942
+ selectionFormatState.blockFormat === option.value
1943
+ ? 'hai-ai-doc-selection-menu-btn--active'
1944
+ : '',
1945
+ )}
1946
+ onclick={() => applyBlockStyle(option.value)}
1947
+ >
1948
+ <span class='hai-ai-doc-selection-menu-label'>
1949
+ <span class='hai-ai-doc-selection-menu-short'>
1950
+ {option.shortLabel}
1951
+ </span>
1952
+ <span>{uiM(option.labelKey)}</span>
1953
+ </span>
1954
+ {#if selectionFormatState.blockFormat === option.value}
1955
+ <span class='hai-ai-doc-selection-menu-check'>✓</span>
1956
+ {/if}
1957
+ </button>
1958
+ {/each}
1959
+ </div>
1960
+ {:else if activeSelectionMenu === 'align' && onapplyalignment}
1961
+ <div
1962
+ class='hai-ai-doc-selection-panel'
1963
+ data-menu-alignment={selectionMenuAlignment}
1964
+ role='menu'
1965
+ tabindex='-1'
1966
+ style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
1967
+ onmousedown={event => event.preventDefault()}
1968
+ >
1969
+ {#each ALIGN_OPTIONS as option}
1970
+ <button
1971
+ type='button'
1972
+ class={cn(
1973
+ 'hai-ai-doc-selection-menu-btn',
1974
+ selectionFormatState.alignment === option.value
1975
+ ? 'hai-ai-doc-selection-menu-btn--active'
1976
+ : '',
1977
+ )}
1978
+ onclick={() => applyAlignment(option.value)}
1979
+ >
1980
+ <span>{uiM(option.labelKey)}</span>
1981
+ {#if selectionFormatState.alignment === option.value}
1982
+ <span class='hai-ai-doc-selection-menu-check'>✓</span>
1983
+ {/if}
1984
+ </button>
1985
+ {/each}
1986
+ </div>
1987
+ {:else if activeSelectionMenu === 'link' && onapplylink}
1988
+ <div
1989
+ class='hai-ai-doc-selection-panel hai-ai-doc-selection-panel--link'
1990
+ data-menu-alignment={selectionMenuAlignment}
1991
+ role='dialog'
1992
+ aria-label={uiM('markdown_link_dialog_label')}
1993
+ tabindex='-1'
1994
+ style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
1995
+ onmousedown={event => event.preventDefault()}
1996
+ >
1997
+ <input
1998
+ class='hai-ai-doc-selection-input'
1999
+ bind:value={linkDraft}
2000
+ aria-label={uiM('markdown_link_input_label')}
2001
+ placeholder={uiM('markdown_link_placeholder')}
2002
+ onkeydown={(event) => {
2003
+ if (event.key === 'Enter') {
2004
+ event.preventDefault()
2005
+ applyLink()
2006
+ }
2007
+ }}
2008
+ />
2009
+ <div class='hai-ai-doc-selection-panel-actions'>
2010
+ <button
2011
+ type='button'
2012
+ class='hai-ai-doc-selection-panel-btn hai-ai-doc-selection-panel-btn--primary'
2013
+ onclick={applyLink}
2014
+ >
2015
+ {uiM('markdown_link_apply')}
2016
+ </button>
2017
+ <button
2018
+ type='button'
2019
+ class='hai-ai-doc-selection-panel-btn'
2020
+ disabled={!selectionFormatState.linkHref}
2021
+ onclick={removeLink}
2022
+ >
2023
+ {uiM('markdown_link_remove')}
2024
+ </button>
2025
+ </div>
2026
+ </div>
2027
+ {:else if activeSelectionMenu === 'color' && onapplycolor}
2028
+ <div
2029
+ class='hai-ai-doc-selection-panel hai-ai-doc-selection-panel--color'
2030
+ data-menu-alignment={selectionMenuAlignment}
2031
+ role='dialog'
2032
+ aria-label={uiM('markdown_color_dialog_label')}
2033
+ tabindex='-1'
2034
+ style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
2035
+ onmousedown={event => event.preventDefault()}
2036
+ >
2037
+ <div class='hai-ai-doc-color-group'>
2038
+ <div class='hai-ai-doc-color-group-title'>
2039
+ {uiM('markdown_text_color')}
2040
+ </div>
2041
+ <div class='hai-ai-doc-color-grid'>
2042
+ {#each TEXT_COLOR_PRESETS as color}
2043
+ <button
2044
+ type='button'
2045
+ class={cn(
2046
+ 'hai-ai-doc-color-swatch',
2047
+ selectionFormatState.textColor === color
2048
+ ? 'hai-ai-doc-color-swatch--active'
2049
+ : '',
2050
+ )}
2051
+ title={color}
2052
+ onclick={() =>
2053
+ applyColor({ target: 'text', value: color })}
2054
+ >
2055
+ <span style={`background:${color};`}></span>
2056
+ </button>
2057
+ {/each}
2058
+ </div>
2059
+ </div>
2060
+ <div class='hai-ai-doc-color-group'>
2061
+ <div class='hai-ai-doc-color-group-title'>
2062
+ {uiM('markdown_background_color')}
2063
+ </div>
2064
+ <div class='hai-ai-doc-color-grid'>
2065
+ {#each BACKGROUND_COLOR_PRESETS as color}
2066
+ <button
2067
+ type='button'
2068
+ class={cn(
2069
+ 'hai-ai-doc-color-swatch',
2070
+ selectionFormatState.backgroundColor === color
2071
+ ? 'hai-ai-doc-color-swatch--active'
2072
+ : '',
2073
+ )}
2074
+ title={color}
2075
+ onclick={() =>
2076
+ applyColor({
2077
+ target: 'background',
2078
+ value: color,
2079
+ })}
2080
+ >
2081
+ <span style={`background:${color};`}></span>
2082
+ </button>
2083
+ {/each}
2084
+ </div>
2085
+ </div>
2086
+ <button
2087
+ type='button'
2088
+ class='hai-ai-doc-selection-panel-btn'
2089
+ onclick={() => {
2090
+ applyColor({ target: 'text', value: null })
2091
+ applyColor({ target: 'background', value: null })
2092
+ }}
2093
+ >
2094
+ {uiM('markdown_reset_default')}
2095
+ </button>
2096
+ </div>
2097
+ {/if}
2098
+ </div>
1238
2099
  </div>
1239
2100
  {/if}
1240
2101
 
@@ -1394,9 +2255,9 @@ ${safeCode}
1394
2255
  }
1395
2256
 
1396
2257
  .hai-ai-doc-toolbar-icon:not(:disabled):hover,
1397
- :global(.hai-ai-doc-toolbar-icon:hover),
2258
+ :global(.hai-ai-doc-toolbar-icon:not(:disabled):hover),
1398
2259
  .hai-ai-doc-toolbar-pill:not(:disabled):hover,
1399
- :global(.hai-ai-doc-toolbar-pill:hover),
2260
+ :global(.hai-ai-doc-toolbar-pill:not(:disabled):hover),
1400
2261
  .hai-ai-doc-toolbar-action:hover,
1401
2262
  .hai-ai-doc-toolbar-close:not(:disabled):hover,
1402
2263
  .hai-ai-doc-version-toggle:hover,
@@ -1407,9 +2268,9 @@ ${safeCode}
1407
2268
  }
1408
2269
 
1409
2270
  .hai-ai-doc-toolbar-icon:not(:disabled):hover,
1410
- :global(.hai-ai-doc-toolbar-icon:hover),
2271
+ :global(.hai-ai-doc-toolbar-icon:not(:disabled):hover),
1411
2272
  .hai-ai-doc-toolbar-pill:not(:disabled):hover,
1412
- :global(.hai-ai-doc-toolbar-pill:hover),
2273
+ :global(.hai-ai-doc-toolbar-pill:not(:disabled):hover),
1413
2274
  .hai-ai-doc-toolbar-close:not(:disabled):hover {
1414
2275
  transform: translateY(-1px);
1415
2276
  box-shadow: 0 12px 28px -22px oklch(var(--bc) / 0.36);
@@ -1420,11 +2281,25 @@ ${safeCode}
1420
2281
  .hai-ai-doc-toolbar-pill:disabled,
1421
2282
  :global(.hai-ai-doc-toolbar-pill:disabled),
1422
2283
  .hai-ai-doc-toolbar-close:disabled {
1423
- opacity: 0.42;
2284
+ opacity: 0.72;
2285
+ color: oklch(var(--bc) / 0.36);
2286
+ border-color: oklch(var(--bc) / 0.12);
2287
+ background: oklch(var(--b2) / 0.58);
1424
2288
  cursor: not-allowed;
1425
2289
  box-shadow: none;
1426
2290
  }
1427
2291
 
2292
+ .hai-ai-doc-toolbar-icon:disabled:hover,
2293
+ :global(.hai-ai-doc-toolbar-icon:disabled:hover),
2294
+ .hai-ai-doc-toolbar-pill:disabled:hover,
2295
+ :global(.hai-ai-doc-toolbar-pill:disabled:hover),
2296
+ .hai-ai-doc-toolbar-close:disabled:hover {
2297
+ cursor: not-allowed;
2298
+ transform: none;
2299
+ box-shadow: none;
2300
+ filter: grayscale(0.18);
2301
+ }
2302
+
1428
2303
  .hai-ai-doc-toolbar-action,
1429
2304
  .hai-ai-doc-version-toggle {
1430
2305
  border: 1px solid oklch(var(--bc) / 0.08);
@@ -1574,7 +2449,8 @@ ${safeCode}
1574
2449
  min-width: 0;
1575
2450
  min-height: 0;
1576
2451
  position: relative;
1577
- z-index: 1;
2452
+ z-index: 2;
2453
+ isolation: isolate;
1578
2454
  }
1579
2455
 
1580
2456
  .hai-ai-doc-scroll {
@@ -1586,47 +2462,66 @@ ${safeCode}
1586
2462
 
1587
2463
  .hai-ai-doc-selection-layer {
1588
2464
  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));
2465
+ z-index: 60;
2466
+ display: block;
2467
+ width: fit-content;
2468
+ max-width: calc(100% - 2rem);
1594
2469
  transform: translateX(-50%);
2470
+ transition:
2471
+ 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);
1595
2474
  pointer-events: none;
1596
2475
  }
1597
2476
 
1598
- .hai-ai-doc-selection-layer[data-placement='top'] {
1599
- flex-direction: column-reverse;
1600
- }
1601
-
1602
2477
  .hai-ai-doc-selection-layer[data-alignment='left'] {
1603
- transform: none;
2478
+ transform: translateX(0);
2479
+ justify-items: start;
1604
2480
  }
1605
2481
 
1606
2482
  .hai-ai-doc-selection-layer[data-alignment='right'] {
1607
2483
  transform: translateX(-100%);
2484
+ justify-items: end;
2485
+ }
2486
+
2487
+ .hai-ai-doc-selection-layer[data-alignment='center'] {
2488
+ justify-items: center;
2489
+ }
2490
+
2491
+ .hai-ai-doc-selection-chrome {
2492
+ position: relative;
2493
+ display: inline-grid;
2494
+ width: fit-content;
2495
+ max-width: min(100%, calc(100vw - 2rem));
1608
2496
  }
1609
2497
 
1610
2498
  .hai-ai-doc-selection-toolbar {
1611
2499
  display: flex;
1612
2500
  align-items: center;
1613
2501
  gap: 0.25rem;
1614
- flex-wrap: wrap;
1615
- max-width: min(calc(100vw - 2rem), 34rem);
2502
+ flex-wrap: nowrap;
2503
+ width: fit-content;
2504
+ max-width: calc(100% - 0.75rem);
2505
+ white-space: nowrap;
1616
2506
  padding: 0.38rem;
1617
2507
  border-radius: 1.2rem;
1618
- background: #fff;
1619
- border: 1px solid color-mix(in srgb, oklch(var(--bc)) 9%, white 91%);
2508
+ background-color: rgb(255 255 255 / 0.96);
2509
+ background: color-mix(in srgb, oklch(var(--b1, 1 0 0)) 94%, white 6%);
2510
+ border: 1px solid rgb(148 163 184 / 0.34);
2511
+ border-color: color-mix(in srgb, oklch(var(--bc, 0.22 0 0)) 16%, white 84%);
1620
2512
  box-shadow:
1621
2513
  0 30px 60px -34px rgb(15 23 42 / 0.28),
1622
2514
  0 14px 26px -18px rgb(15 23 42 / 0.16),
1623
2515
  0 0 0 1px rgb(255 255 255 / 0.94) inset;
1624
2516
  pointer-events: auto;
1625
2517
  isolation: isolate;
2518
+ position: relative;
2519
+ z-index: 2;
1626
2520
  }
1627
2521
 
1628
2522
  .hai-ai-doc-selection-chip,
1629
- .hai-ai-doc-selection-btn {
2523
+ .hai-ai-doc-selection-btn,
2524
+ .hai-ai-doc-selection-trigger {
1630
2525
  min-height: 2.25rem;
1631
2526
  border-radius: 0.9rem;
1632
2527
  transition:
@@ -1640,6 +2535,7 @@ ${safeCode}
1640
2535
  display: inline-flex;
1641
2536
  align-items: center;
1642
2537
  gap: 0.48rem;
2538
+ flex-shrink: 0;
1643
2539
  padding: 0 0.88rem;
1644
2540
  border: 1px solid oklch(var(--p) / 0.16);
1645
2541
  background: oklch(var(--p) / 0.1);
@@ -1654,9 +2550,16 @@ ${safeCode}
1654
2550
  fill: currentColor;
1655
2551
  }
1656
2552
 
2553
+ .hai-ai-doc-selection-chip--active {
2554
+ border-color: oklch(var(--p) / 0.28);
2555
+ background: oklch(var(--p) / 0.16);
2556
+ color: oklch(var(--bc));
2557
+ }
2558
+
1657
2559
  .hai-ai-doc-selection-chip:not(:disabled):hover,
1658
2560
  .hai-ai-doc-selection-btn:hover,
1659
- .hai-ai-doc-rewrite-menu button:hover {
2561
+ .hai-ai-doc-selection-trigger:hover,
2562
+ .hai-ai-doc-rewrite-menu-btn:hover {
1660
2563
  transform: translateY(-1px);
1661
2564
  }
1662
2565
 
@@ -1668,6 +2571,7 @@ ${safeCode}
1668
2571
  .hai-ai-doc-selection-btn {
1669
2572
  width: 2.25rem;
1670
2573
  min-width: 2.25rem;
2574
+ flex-shrink: 0;
1671
2575
  display: inline-flex;
1672
2576
  align-items: center;
1673
2577
  justify-content: center;
@@ -1679,21 +2583,61 @@ ${safeCode}
1679
2583
  }
1680
2584
 
1681
2585
  .hai-ai-doc-selection-btn-wide {
1682
- min-width: 2.25rem;
2586
+ width: auto;
2587
+ min-width: 4.6rem;
2588
+ gap: 0.42rem;
2589
+ padding: 0 0.74rem;
2590
+ }
2591
+
2592
+ .hai-ai-doc-selection-trigger {
2593
+ min-width: 3.4rem;
2594
+ display: inline-flex;
2595
+ align-items: center;
2596
+ gap: 0.34rem;
2597
+ padding: 0 0.66rem;
2598
+ border: 1px solid transparent;
2599
+ background: transparent;
2600
+ color: oklch(var(--bc) / 0.78);
2601
+ cursor: pointer;
1683
2602
  }
1684
2603
 
1685
- .hai-ai-doc-selection-btn:hover {
2604
+ .hai-ai-doc-selection-btn:hover,
2605
+ .hai-ai-doc-selection-trigger:hover {
1686
2606
  border-color: oklch(var(--bc) / 0.1);
1687
2607
  background: oklch(var(--bc) / 0.045);
1688
2608
  color: oklch(var(--bc));
1689
2609
  }
1690
2610
 
2611
+ .hai-ai-doc-selection-btn--active,
2612
+ .hai-ai-doc-selection-trigger--active {
2613
+ border-color: oklch(var(--p) / 0.22);
2614
+ background: oklch(var(--p) / 0.12);
2615
+ color: oklch(var(--bc));
2616
+ }
2617
+
1691
2618
  .hai-ai-doc-selection-btn svg {
1692
2619
  width: 1rem;
1693
2620
  height: 1rem;
1694
2621
  fill: currentColor;
1695
2622
  }
1696
2623
 
2624
+ .hai-ai-doc-selection-trigger svg {
2625
+ width: 1rem;
2626
+ height: 1rem;
2627
+ fill: currentColor;
2628
+ }
2629
+
2630
+ .hai-ai-doc-selection-trigger-label {
2631
+ font-weight: 600;
2632
+ line-height: 1;
2633
+ }
2634
+
2635
+ .hai-ai-doc-selection-trigger-chevron {
2636
+ width: 0.8rem !important;
2637
+ height: 0.8rem !important;
2638
+ opacity: 0.72;
2639
+ }
2640
+
1697
2641
  .hai-ai-doc-selection-divider {
1698
2642
  width: 1px;
1699
2643
  height: 1.45rem;
@@ -1705,6 +2649,7 @@ ${safeCode}
1705
2649
  .hai-ai-doc-selection-highlight-icon {
1706
2650
  position: relative;
1707
2651
  font-weight: 700;
2652
+ color: var(--hai-ai-doc-current-color, currentColor);
1708
2653
  }
1709
2654
 
1710
2655
  .hai-ai-doc-selection-highlight-icon::after {
@@ -1715,41 +2660,213 @@ ${safeCode}
1715
2660
  bottom: -0.02rem;
1716
2661
  height: 0.36rem;
1717
2662
  border-radius: 9999px;
1718
- background: oklch(var(--wa, 0.9 0.14 90) / 0.45);
2663
+ background: color-mix(
2664
+ in srgb,
2665
+ var(--hai-ai-doc-current-color, oklch(var(--wa, 0.9 0.14 90))) 22%,
2666
+ white 78%
2667
+ );
1719
2668
  z-index: -1;
1720
2669
  }
1721
2670
 
2671
+ .hai-ai-doc-selection-panel,
1722
2672
  .hai-ai-doc-rewrite-menu {
1723
- width: 13.75rem;
2673
+ position: absolute;
2674
+ left: var(--hai-ai-doc-selection-menu-left, 0px);
2675
+ top: calc(100% + 0.45rem);
1724
2676
  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%);
2677
+ gap: 0.34rem;
2678
+ width: min(18rem, calc(100vw - 2rem));
2679
+ max-width: calc(100vw - 2rem);
2680
+ padding: 0.42rem;
2681
+ border-radius: 1rem;
2682
+ background-color: rgb(255 255 255 / 0.96);
2683
+ background: color-mix(in srgb, oklch(var(--b1, 1 0 0)) 94%, white 6%);
2684
+ border: 1px solid rgb(148 163 184 / 0.3);
2685
+ border-color: color-mix(in srgb, oklch(var(--bc, 0.22 0 0)) 14%, white 86%);
1730
2686
  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;
2687
+ 0 24px 40px -28px rgb(15 23 42 / 0.25),
2688
+ 0 10px 20px -16px rgb(15 23 42 / 0.14);
1734
2689
  pointer-events: auto;
1735
2690
  isolation: isolate;
2691
+ z-index: 3;
1736
2692
  }
1737
2693
 
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;
2694
+ .hai-ai-doc-rewrite-menu {
2695
+ width: fit-content;
2696
+ max-width: min(22rem, calc(100vw - 2rem));
2697
+ grid-template-columns: repeat(auto-fit, minmax(7.2rem, 1fr));
2698
+ }
2699
+
2700
+ .hai-ai-doc-selection-layer[data-placement='bottom'] .hai-ai-doc-selection-panel,
2701
+ .hai-ai-doc-selection-layer[data-placement='bottom'] .hai-ai-doc-rewrite-menu {
2702
+ top: auto;
2703
+ bottom: calc(100% + 0.45rem);
2704
+ }
2705
+
2706
+ .hai-ai-doc-selection-panel[data-menu-alignment='left'],
2707
+ .hai-ai-doc-rewrite-menu[data-menu-alignment='left'] {
2708
+ transform: translateX(0);
2709
+ }
2710
+
2711
+ .hai-ai-doc-selection-panel[data-menu-alignment='center'],
2712
+ .hai-ai-doc-rewrite-menu[data-menu-alignment='center'] {
2713
+ transform: translateX(-50%);
2714
+ }
2715
+
2716
+ .hai-ai-doc-selection-panel[data-menu-alignment='right'],
2717
+ .hai-ai-doc-rewrite-menu[data-menu-alignment='right'] {
2718
+ transform: translateX(-100%);
2719
+ }
2720
+
2721
+ .hai-ai-doc-selection-panel--link,
2722
+ .hai-ai-doc-selection-panel--color {
2723
+ width: min(16rem, calc(100vw - 2rem));
2724
+ }
2725
+
2726
+ .hai-ai-doc-selection-menu-btn,
2727
+ .hai-ai-doc-selection-panel-btn,
2728
+ .hai-ai-doc-rewrite-menu-btn {
2729
+ min-height: 2rem;
2730
+ display: inline-flex;
2731
+ align-items: center;
2732
+ justify-content: space-between;
2733
+ gap: 0.6rem;
2734
+ width: 100%;
2735
+ padding: 0 0.72rem;
2736
+ border-radius: 0.8rem;
2737
+ border: 1px solid oklch(var(--bc) / 0.14);
2738
+ background: color-mix(in srgb, oklch(var(--b1)) 92%, white 8%);
2739
+ color: oklch(var(--bc) / 0.86);
2740
+ font-size: 0.82rem;
2741
+ line-height: 1;
1745
2742
  cursor: pointer;
2743
+ transition:
2744
+ background-color 0.14s ease,
2745
+ border-color 0.14s ease,
2746
+ color 0.14s ease,
2747
+ transform 0.14s ease;
2748
+ }
2749
+
2750
+ .hai-ai-doc-selection-menu-btn--active,
2751
+ .hai-ai-doc-selection-panel-btn--primary {
2752
+ border-color: oklch(var(--p) / 0.28);
2753
+ background: oklch(var(--p) / 0.12);
2754
+ color: oklch(var(--bc));
2755
+ }
2756
+
2757
+ .hai-ai-doc-selection-menu-btn:hover,
2758
+ .hai-ai-doc-selection-panel-btn:hover,
2759
+ .hai-ai-doc-rewrite-menu-btn:hover:not(:disabled) {
2760
+ border-color: oklch(var(--bc) / 0.22);
2761
+ background: color-mix(in srgb, oklch(var(--b2)) 68%, white 32%);
2762
+ color: oklch(var(--bc));
2763
+ transform: translateY(-1px);
2764
+ }
2765
+
2766
+ .hai-ai-doc-selection-menu-label {
2767
+ display: inline-flex;
2768
+ align-items: center;
2769
+ gap: 0.62rem;
2770
+ }
2771
+
2772
+ .hai-ai-doc-selection-menu-short {
2773
+ min-width: 2rem;
2774
+ color: oklch(var(--p) / 0.92);
2775
+ font-weight: 700;
2776
+ }
2777
+
2778
+ .hai-ai-doc-selection-menu-check {
2779
+ color: oklch(var(--p) / 0.88);
2780
+ font-weight: 700;
1746
2781
  }
1747
2782
 
1748
- .hai-ai-doc-rewrite-menu button:disabled {
2783
+ .hai-ai-doc-selection-input {
2784
+ width: 100%;
2785
+ min-height: 2.25rem;
2786
+ padding: 0 0.78rem;
2787
+ border-radius: 0.85rem;
2788
+ border: 1px solid oklch(var(--bc) / 0.16);
2789
+ background: color-mix(in srgb, oklch(var(--b1)) 94%, white 6%);
2790
+ color: oklch(var(--bc));
2791
+ font-size: 0.84rem;
2792
+ outline: none;
2793
+ }
2794
+
2795
+ .hai-ai-doc-selection-input:focus {
2796
+ border-color: oklch(var(--p) / 0.34);
2797
+ box-shadow: 0 0 0 3px oklch(var(--p) / 0.12);
2798
+ }
2799
+
2800
+ .hai-ai-doc-selection-panel-actions {
2801
+ display: flex;
2802
+ gap: 0.4rem;
2803
+ }
2804
+
2805
+ .hai-ai-doc-selection-panel-actions .hai-ai-doc-selection-panel-btn {
2806
+ flex: 1 1 0;
2807
+ }
2808
+
2809
+ .hai-ai-doc-selection-panel-btn:disabled,
2810
+ .hai-ai-doc-rewrite-menu-btn:disabled {
1749
2811
  opacity: 0.55;
1750
2812
  cursor: default;
1751
2813
  }
1752
2814
 
2815
+ .hai-ai-doc-color-group {
2816
+ display: grid;
2817
+ gap: 0.42rem;
2818
+ }
2819
+
2820
+ .hai-ai-doc-color-group-title {
2821
+ font-size: 0.78rem;
2822
+ font-weight: 600;
2823
+ color: oklch(var(--bc) / 0.68);
2824
+ }
2825
+
2826
+ .hai-ai-doc-color-grid {
2827
+ display: grid;
2828
+ grid-template-columns: repeat(4, minmax(0, 1fr));
2829
+ gap: 0.42rem;
2830
+ }
2831
+
2832
+ .hai-ai-doc-color-swatch {
2833
+ width: 100%;
2834
+ aspect-ratio: 1;
2835
+ display: inline-flex;
2836
+ align-items: center;
2837
+ justify-content: center;
2838
+ padding: 0.18rem;
2839
+ border-radius: 0.7rem;
2840
+ border: 1px solid oklch(var(--bc) / 0.12);
2841
+ background: color-mix(in srgb, oklch(var(--b1)) 92%, white 8%);
2842
+ cursor: pointer;
2843
+ transition:
2844
+ transform 0.14s ease,
2845
+ border-color 0.14s ease,
2846
+ box-shadow 0.14s ease;
2847
+ }
2848
+
2849
+ .hai-ai-doc-color-swatch:hover {
2850
+ transform: translateY(-1px);
2851
+ border-color: oklch(var(--bc) / 0.2);
2852
+ }
2853
+
2854
+ .hai-ai-doc-color-swatch--active {
2855
+ border-color: oklch(var(--p) / 0.34);
2856
+ box-shadow: 0 0 0 3px oklch(var(--p) / 0.12);
2857
+ }
2858
+
2859
+ .hai-ai-doc-color-swatch span {
2860
+ width: 100%;
2861
+ height: 100%;
2862
+ border-radius: 0.5rem;
2863
+ border: 1px solid rgb(15 23 42 / 0.08);
2864
+ }
2865
+
2866
+ .hai-ai-doc-rewrite-menu-btn {
2867
+ justify-content: center;
2868
+ }
2869
+
1753
2870
  .hai-markdown {
1754
2871
  line-height: 1.9;
1755
2872
  color: inherit;
@@ -1829,6 +2946,10 @@ ${safeCode}
1829
2946
  margin-bottom: 1em;
1830
2947
  }
1831
2948
 
2949
+ .hai-markdown :global(.hai-md-align-block) {
2950
+ width: 100%;
2951
+ }
2952
+
1832
2953
  .hai-markdown :global(a) {
1833
2954
  color: oklch(var(--p));
1834
2955
  text-decoration: none;
@@ -1852,6 +2973,23 @@ ${safeCode}
1852
2973
  color: oklch(var(--bc) / 0.55);
1853
2974
  }
1854
2975
 
2976
+ .hai-markdown :global(u) {
2977
+ text-decoration-thickness: 0.08em;
2978
+ text-underline-offset: 0.14em;
2979
+ }
2980
+
2981
+ .hai-markdown :global(mark) {
2982
+ padding: 0 0.12em;
2983
+ border-radius: 0.25rem;
2984
+ background: oklch(var(--wa, 0.9 0.14 90) / 0.34);
2985
+ color: inherit;
2986
+ }
2987
+
2988
+ .hai-markdown :global(.hai-md-inline-style) {
2989
+ border-radius: 0.2rem;
2990
+ padding: 0 0.04em;
2991
+ }
2992
+
1855
2993
  .hai-markdown :global(:not(pre) > code) {
1856
2994
  padding: 0.2em 0.4em;
1857
2995
  margin: 0 0.1em;
@@ -2203,7 +3341,7 @@ ${safeCode}
2203
3341
 
2204
3342
  .hai-ai-doc-selection-toolbar {
2205
3343
  max-width: calc(100vw - 2rem);
2206
- overflow: auto;
3344
+ overflow: hidden;
2207
3345
  }
2208
3346
  }
2209
3347
  </style>