@linto-ai/transcript-ui-ui 0.9.0

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.
Files changed (39) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +17 -0
  3. package/package.json +55 -0
  4. package/src/atoms/Badge.vue +26 -0
  5. package/src/atoms/Button.vue +264 -0
  6. package/src/atoms/CodeBlock.vue +98 -0
  7. package/src/atoms/CopyButton.vue +98 -0
  8. package/src/atoms/EditableText.vue +115 -0
  9. package/src/atoms/EditorCheckbox.vue +66 -0
  10. package/src/atoms/EditorIcon.vue +59 -0
  11. package/src/atoms/MarkdownEditor.vue +542 -0
  12. package/src/atoms/MarkdownView.vue +159 -0
  13. package/src/atoms/PopoverList.vue +92 -0
  14. package/src/atoms/SelectableListItem.vue +183 -0
  15. package/src/atoms/SpeakerIndicator.vue +19 -0
  16. package/src/atoms/SwitchToggle.vue +90 -0
  17. package/src/atoms/UserAvatar.vue +38 -0
  18. package/src/atoms/icons.ts +108 -0
  19. package/src/index.ts +36 -0
  20. package/src/molecules/DocumentArticle.vue +169 -0
  21. package/src/molecules/DownloadMenu.vue +48 -0
  22. package/src/molecules/FormInput.vue +510 -0
  23. package/src/molecules/SpeakerMenu.vue +43 -0
  24. package/src/molecules/Tabs.vue +119 -0
  25. package/src/molecules/TurnTextEditor.vue +93 -0
  26. package/src/styles/base.css +110 -0
  27. package/src/styles/fonts.css +45 -0
  28. package/src/styles/popover-list.css +67 -0
  29. package/src/styles/variables.css +131 -0
  30. package/src/turndown-plugin-gfm.d.ts +10 -0
  31. package/src/utils/computeInitials.ts +10 -0
  32. package/src/utils/computeSelectionOffset.ts +10 -0
  33. package/src/utils/computeTextOffsetInContainer.ts +54 -0
  34. package/src/utils/highlight.ts +46 -0
  35. package/src/utils/markdown.ts +56 -0
  36. package/src/utils/placeCaretAt.ts +23 -0
  37. package/src/utils/shadowAwareSelection.ts +52 -0
  38. package/src/utils/test/computeInitials.test.ts +15 -0
  39. package/src/utils/test/computeTextOffsetInContainer.test.ts +53 -0
@@ -0,0 +1,66 @@
1
+ <script setup lang="ts">
2
+ import { CheckboxRoot, CheckboxIndicator } from "reka-ui"
3
+ import { Check } from "lucide-vue-next"
4
+
5
+ defineProps<{
6
+ modelValue: boolean
7
+ ariaLabel?: string
8
+ }>()
9
+
10
+ defineEmits<{
11
+ "update:modelValue": [value: boolean]
12
+ }>()
13
+ </script>
14
+
15
+ <template>
16
+ <CheckboxRoot
17
+ :model-value="modelValue"
18
+ :aria-label="ariaLabel"
19
+ class="checkbox"
20
+ @update:model-value="$emit('update:modelValue', !!$event)"
21
+ @click.stop>
22
+ <CheckboxIndicator class="checkbox-indicator">
23
+ <Check :size="12" :stroke-width="3" />
24
+ </CheckboxIndicator>
25
+ </CheckboxRoot>
26
+ </template>
27
+
28
+ <style scoped>
29
+ .checkbox {
30
+ all: unset;
31
+ width: 16px;
32
+ height: 16px;
33
+ flex-shrink: 0;
34
+ border: 1.5px solid var(--color-border);
35
+ border-radius: var(--radius-sm);
36
+ background-color: var(--color-surface);
37
+ display: inline-flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ cursor: pointer;
41
+ transition:
42
+ background-color var(--transition-duration),
43
+ border-color var(--transition-duration);
44
+ }
45
+
46
+ .checkbox:hover {
47
+ border-color: var(--color-primary);
48
+ }
49
+
50
+ .checkbox:focus-visible {
51
+ outline: 2px solid var(--color-primary);
52
+ outline-offset: 2px;
53
+ }
54
+
55
+ .checkbox[data-state="checked"] {
56
+ background-color: var(--color-primary);
57
+ border-color: var(--color-primary);
58
+ }
59
+
60
+ .checkbox-indicator {
61
+ color: var(--color-white, #fff);
62
+ display: inline-flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ }
66
+ </style>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue"
3
+ import { resolveIcon } from "./icons"
4
+
5
+ const props = defineProps<{
6
+ name: string
7
+ size?: number
8
+ spin?: boolean
9
+ }>()
10
+
11
+ const Comp = computed(() => resolveIcon(props.name))
12
+
13
+ const style = computed(() =>
14
+ props.size != null
15
+ ? { width: `${props.size}px`, height: `${props.size}px` }
16
+ : undefined,
17
+ )
18
+ </script>
19
+
20
+ <template>
21
+ <component
22
+ v-if="Comp"
23
+ :is="Comp"
24
+ :style="style"
25
+ :class="['editor-icon', { 'editor-icon--spin': spin }]"
26
+ aria-hidden="true" />
27
+ <span v-else class="editor-icon editor-icon--missing" aria-hidden="true">?</span>
28
+ </template>
29
+
30
+ <style scoped>
31
+ .editor-icon {
32
+ flex-shrink: 0;
33
+ }
34
+
35
+ .editor-icon--missing {
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ opacity: 0.5;
40
+ font-size: 1em;
41
+ line-height: 1;
42
+ }
43
+
44
+ .editor-icon--spin {
45
+ animation: editor-icon-spin 1s linear infinite;
46
+ }
47
+
48
+ @keyframes editor-icon-spin {
49
+ to {
50
+ transform: rotate(360deg);
51
+ }
52
+ }
53
+
54
+ @media (prefers-reduced-motion: reduce) {
55
+ .editor-icon--spin {
56
+ animation: none;
57
+ }
58
+ }
59
+ </style>
@@ -0,0 +1,542 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ onBeforeUnmount,
4
+ onMounted,
5
+ reactive,
6
+ useTemplateRef,
7
+ watch,
8
+ } from "vue"
9
+ import TurndownService from "turndown"
10
+ import { gfm } from "turndown-plugin-gfm"
11
+ import Button from "./Button.vue"
12
+ import { useI18n } from "@linto-ai/transcript-ui-i18n"
13
+ import { renderMarkdown } from "../utils/markdown"
14
+ import { getShadowAwareSelection } from "../utils/shadowAwareSelection"
15
+
16
+ const props = withDefaults(
17
+ defineProps<{
18
+ modelValue: string
19
+ disabled?: boolean
20
+ }>(),
21
+ { disabled: false },
22
+ )
23
+
24
+ const emit = defineEmits<{
25
+ "update:modelValue": [value: string]
26
+ }>()
27
+
28
+ const { t } = useI18n()
29
+
30
+ // ── Markdown / HTML converters ────────────────────────────────────────
31
+
32
+ const turndown = new TurndownService({
33
+ headingStyle: "atx",
34
+ hr: "---",
35
+ bulletListMarker: "-",
36
+ codeBlockStyle: "fenced",
37
+ emDelimiter: "*",
38
+ strongDelimiter: "**",
39
+ })
40
+ turndown.use(gfm)
41
+
42
+ function mdToHtml(md: string): string {
43
+ return renderMarkdown(md)
44
+ }
45
+
46
+ function htmlToMd(html: string): string {
47
+ if (!html) return ""
48
+ return turndown.turndown(html)
49
+ }
50
+
51
+ // ── State ─────────────────────────────────────────────────────────────
52
+
53
+ const editorRef = useTemplateRef<HTMLDivElement>("editorEl")
54
+
55
+ const toolbarState = reactive({
56
+ bold: false,
57
+ italic: false,
58
+ strike: false,
59
+ h1: false,
60
+ h2: false,
61
+ h3: false,
62
+ bulletList: false,
63
+ orderedList: false,
64
+ blockquote: false,
65
+ codeBlock: false,
66
+ })
67
+
68
+ let lastEmittedMarkdown: string | null = null
69
+ let emitFrame: number | null = null
70
+ let selectionListener: (() => void) | null = null
71
+
72
+ // ── Core ──────────────────────────────────────────────────────────────
73
+
74
+ function setHtml(html: string): void {
75
+ const el = editorRef.value
76
+ if (!el) return
77
+ el.innerHTML = html
78
+ }
79
+
80
+ function getCurrentMarkdown(): string {
81
+ const el = editorRef.value
82
+ if (!el) return ""
83
+ return htmlToMd(el.innerHTML)
84
+ }
85
+
86
+ // ── Events ────────────────────────────────────────────────────────────
87
+
88
+ function onInput(): void {
89
+ if (emitFrame !== null) cancelAnimationFrame(emitFrame)
90
+ emitFrame = requestAnimationFrame(() => {
91
+ emitFrame = null
92
+ const md = getCurrentMarkdown()
93
+ lastEmittedMarkdown = md
94
+ if (md !== props.modelValue) emit("update:modelValue", md)
95
+ })
96
+ }
97
+
98
+ function onKeydown(e: KeyboardEvent): void {
99
+ // Shift+Enter → line break (vs new paragraph on plain Enter)
100
+ if (e.key === "Enter" && e.shiftKey) {
101
+ e.preventDefault()
102
+ document.execCommand("insertLineBreak")
103
+ }
104
+ }
105
+
106
+ function onPaste(e: ClipboardEvent): void {
107
+ e.preventDefault()
108
+ const clipboard = e.clipboardData
109
+ if (!clipboard) return
110
+ const text = clipboard.getData("text/plain")
111
+ if (!text) return
112
+
113
+ const looksLikeMd =
114
+ /^#{1,6}\s|^\s*[-*+]\s|^\s*\d+\.\s|^\s*>|```|\*\*|__|\[.*\]\(/m.test(text)
115
+
116
+ if (looksLikeMd) {
117
+ document.execCommand("insertHTML", false, mdToHtml(text))
118
+ } else {
119
+ document.execCommand("insertText", false, text)
120
+ }
121
+ }
122
+
123
+ // ── Toolbar commands ──────────────────────────────────────────────────
124
+
125
+ function focusEditor(): void {
126
+ editorRef.value?.focus()
127
+ }
128
+
129
+ function execCmd(command: string): void {
130
+ focusEditor()
131
+ document.execCommand(command)
132
+ updateToolbarState()
133
+ onInput()
134
+ }
135
+
136
+ function toggleBlock(tag: "H1" | "H2" | "H3"): void {
137
+ focusEditor()
138
+ const sel = getShadowAwareSelection(editorRef.value)
139
+ const inBlock = sel?.rangeCount && findAncestor(sel.anchorNode, tag)
140
+ document.execCommand("formatBlock", false, inBlock ? "P" : tag)
141
+ updateToolbarState()
142
+ onInput()
143
+ }
144
+
145
+ function toggleBlockquote(): void {
146
+ focusEditor()
147
+ const sel = getShadowAwareSelection(editorRef.value)
148
+ if (!sel || !sel.rangeCount) return
149
+
150
+ const block = getParentBlock(sel.anchorNode)
151
+ if (block && block.tagName === "BLOCKQUOTE") {
152
+ const parent = block.parentNode
153
+ if (!parent) return
154
+ while (block.firstChild) {
155
+ parent.insertBefore(block.firstChild, block)
156
+ }
157
+ parent.removeChild(block)
158
+ } else {
159
+ document.execCommand("formatBlock", false, "BLOCKQUOTE")
160
+ }
161
+ updateToolbarState()
162
+ onInput()
163
+ }
164
+
165
+ function toggleCodeBlock(): void {
166
+ focusEditor()
167
+ const sel = getShadowAwareSelection(editorRef.value)
168
+ if (!sel || !sel.rangeCount) return
169
+
170
+ const pre = findAncestor(sel.anchorNode, "PRE")
171
+ if (pre) {
172
+ const p = document.createElement("p")
173
+ p.textContent = pre.textContent ?? ""
174
+ pre.parentNode?.replaceChild(p, pre)
175
+ const range = document.createRange()
176
+ range.selectNodeContents(p)
177
+ range.collapse(false)
178
+ sel.removeAllRanges()
179
+ sel.addRange(range)
180
+ } else {
181
+ const range = sel.getRangeAt(0)
182
+ const text = range.toString() || "\n"
183
+ const wrapper = document.createElement("pre")
184
+ const code = document.createElement("code")
185
+ code.textContent = text
186
+ wrapper.appendChild(code)
187
+ range.deleteContents()
188
+ range.insertNode(wrapper)
189
+ const newRange = document.createRange()
190
+ newRange.setStartAfter(wrapper)
191
+ newRange.collapse(true)
192
+ sel.removeAllRanges()
193
+ sel.addRange(newRange)
194
+ }
195
+ updateToolbarState()
196
+ onInput()
197
+ }
198
+
199
+ // ── Selection / toolbar state ─────────────────────────────────────────
200
+
201
+ function startSelectionListener(): void {
202
+ if (selectionListener) return
203
+ selectionListener = () => updateToolbarState()
204
+ document.addEventListener("selectionchange", selectionListener)
205
+ updateToolbarState()
206
+ }
207
+
208
+ function stopSelectionListener(): void {
209
+ if (selectionListener) {
210
+ document.removeEventListener("selectionchange", selectionListener)
211
+ selectionListener = null
212
+ }
213
+ }
214
+
215
+ function updateToolbarState(): void {
216
+ toolbarState.bold = document.queryCommandState("bold")
217
+ toolbarState.italic = document.queryCommandState("italic")
218
+ toolbarState.strike = document.queryCommandState("strikeThrough")
219
+ toolbarState.h1 = isInAncestor("H1")
220
+ toolbarState.h2 = isInAncestor("H2")
221
+ toolbarState.h3 = isInAncestor("H3")
222
+ toolbarState.bulletList = document.queryCommandState("insertUnorderedList")
223
+ toolbarState.orderedList = document.queryCommandState("insertOrderedList")
224
+ toolbarState.blockquote = isInAncestor("BLOCKQUOTE")
225
+ toolbarState.codeBlock = isInAncestor("PRE")
226
+ }
227
+
228
+ function isInAncestor(tag: string): boolean {
229
+ const sel = getShadowAwareSelection(editorRef.value)
230
+ if (!sel || !sel.rangeCount) return false
231
+ return !!findAncestor(sel.anchorNode, tag)
232
+ }
233
+
234
+ // ── DOM helpers ───────────────────────────────────────────────────────
235
+
236
+ function getParentBlock(node: Node | null): HTMLElement | null {
237
+ const el = editorRef.value
238
+ let n: Node | null = node
239
+ while (n && n !== el) {
240
+ if (
241
+ n.nodeType === 1 &&
242
+ /^(P|H[1-6]|BLOCKQUOTE|PRE|UL|OL|LI|DIV)$/.test(
243
+ (n as HTMLElement).tagName,
244
+ )
245
+ ) {
246
+ return n as HTMLElement
247
+ }
248
+ n = n.parentNode
249
+ }
250
+ return null
251
+ }
252
+
253
+ function findAncestor(node: Node | null, tag: string): HTMLElement | null {
254
+ const el = editorRef.value
255
+ let n: Node | null = node
256
+ while (n && n !== el) {
257
+ if (n.nodeType === 1 && (n as HTMLElement).tagName === tag) {
258
+ return n as HTMLElement
259
+ }
260
+ n = n.parentNode
261
+ }
262
+ return null
263
+ }
264
+
265
+ function variantFor(active: boolean): "secondary" | "tertiary" {
266
+ return active ? "secondary" : "tertiary"
267
+ }
268
+
269
+ // ── Lifecycle / watchers ──────────────────────────────────────────────
270
+
271
+ onMounted(() => {
272
+ setHtml(mdToHtml(props.modelValue || ""))
273
+ })
274
+
275
+ onBeforeUnmount(() => {
276
+ stopSelectionListener()
277
+ if (emitFrame !== null) cancelAnimationFrame(emitFrame)
278
+ })
279
+
280
+ watch(
281
+ () => props.modelValue,
282
+ (next) => {
283
+ // Skip echo: the parent re-sent what we just emitted.
284
+ if (next === lastEmittedMarkdown) return
285
+ // Skip if the editor already shows this content.
286
+ if (next === getCurrentMarkdown()) return
287
+ // Genuine external update (version switch, gateway content arrival).
288
+ setHtml(mdToHtml(next || ""))
289
+ lastEmittedMarkdown = null
290
+ },
291
+ )
292
+ </script>
293
+
294
+ <template>
295
+ <div class="markdown-editor">
296
+ <div
297
+ v-if="!disabled"
298
+ class="markdown-editor__toolbar"
299
+ role="toolbar"
300
+ :aria-label="t('mdToolbar.label')">
301
+ <Button
302
+ size="sm"
303
+ :variant="variantFor(toolbarState.h1)"
304
+ icon="heading-1"
305
+ :aria-label="t('mdToolbar.h1')"
306
+ :title="t('mdToolbar.h1')"
307
+ @click="toggleBlock('H1')" />
308
+ <Button
309
+ size="sm"
310
+ :variant="variantFor(toolbarState.h2)"
311
+ icon="heading-2"
312
+ :aria-label="t('mdToolbar.h2')"
313
+ :title="t('mdToolbar.h2')"
314
+ @click="toggleBlock('H2')" />
315
+ <Button
316
+ size="sm"
317
+ :variant="variantFor(toolbarState.h3)"
318
+ icon="heading-3"
319
+ :aria-label="t('mdToolbar.h3')"
320
+ :title="t('mdToolbar.h3')"
321
+ @click="toggleBlock('H3')" />
322
+
323
+ <span class="markdown-editor__separator" aria-hidden="true" />
324
+
325
+ <Button
326
+ size="sm"
327
+ :variant="variantFor(toolbarState.bold)"
328
+ icon="bold"
329
+ :aria-label="t('mdToolbar.bold')"
330
+ :title="t('mdToolbar.bold')"
331
+ @click="execCmd('bold')" />
332
+ <Button
333
+ size="sm"
334
+ :variant="variantFor(toolbarState.italic)"
335
+ icon="italic"
336
+ :aria-label="t('mdToolbar.italic')"
337
+ :title="t('mdToolbar.italic')"
338
+ @click="execCmd('italic')" />
339
+
340
+ <span class="markdown-editor__separator" aria-hidden="true" />
341
+
342
+ <Button
343
+ size="sm"
344
+ :variant="variantFor(toolbarState.bulletList)"
345
+ icon="list"
346
+ :aria-label="t('mdToolbar.bulletList')"
347
+ :title="t('mdToolbar.bulletList')"
348
+ @click="execCmd('insertUnorderedList')" />
349
+ <Button
350
+ size="sm"
351
+ :variant="variantFor(toolbarState.orderedList)"
352
+ icon="list-ordered"
353
+ :aria-label="t('mdToolbar.orderedList')"
354
+ :title="t('mdToolbar.orderedList')"
355
+ @click="execCmd('insertOrderedList')" />
356
+ <Button
357
+ size="sm"
358
+ :variant="variantFor(toolbarState.blockquote)"
359
+ icon="quote"
360
+ :aria-label="t('mdToolbar.quote')"
361
+ :title="t('mdToolbar.quote')"
362
+ @click="toggleBlockquote" />
363
+
364
+ <span class="markdown-editor__separator" aria-hidden="true" />
365
+
366
+ <Button
367
+ size="sm"
368
+ :variant="variantFor(toolbarState.codeBlock)"
369
+ icon="code-block"
370
+ :aria-label="t('mdToolbar.codeBlock')"
371
+ :title="t('mdToolbar.codeBlock')"
372
+ @click="toggleCodeBlock" />
373
+
374
+ <span class="markdown-editor__separator" aria-hidden="true" />
375
+
376
+ <Button
377
+ size="sm"
378
+ variant="tertiary"
379
+ icon="undo"
380
+ :aria-label="t('mdToolbar.undo')"
381
+ :title="t('mdToolbar.undo')"
382
+ @click="execCmd('undo')" />
383
+ <Button
384
+ size="sm"
385
+ variant="tertiary"
386
+ icon="redo"
387
+ :aria-label="t('mdToolbar.redo')"
388
+ :title="t('mdToolbar.redo')"
389
+ @click="execCmd('redo')" />
390
+ </div>
391
+
392
+ <div
393
+ ref="editorEl"
394
+ class="markdown-editor__content"
395
+ :contenteditable="!disabled"
396
+ @input="onInput"
397
+ @keydown="onKeydown"
398
+ @paste="onPaste"
399
+ @focus="startSelectionListener"
400
+ @blur="stopSelectionListener" />
401
+ </div>
402
+ </template>
403
+
404
+ <style scoped>
405
+ .markdown-editor {
406
+ display: flex;
407
+ flex-direction: column;
408
+ font-family: var(--font-family);
409
+ font-size: var(--font-size-base);
410
+ line-height: var(--line-height);
411
+ color: var(--color-text-primary);
412
+ }
413
+
414
+ .markdown-editor__toolbar {
415
+ display: flex;
416
+ flex-wrap: wrap;
417
+ align-items: center;
418
+ gap: var(--spacing-xs);
419
+ padding: var(--spacing-xs) var(--spacing-md);
420
+ border-bottom: 1px solid var(--color-border);
421
+ background-color: var(--color-surface);
422
+ position: sticky;
423
+ top: 0;
424
+ z-index: 1;
425
+ }
426
+
427
+ .markdown-editor__separator {
428
+ width: 1px;
429
+ height: 20px;
430
+ background-color: var(--color-border);
431
+ margin: 0 var(--spacing-xs);
432
+ }
433
+
434
+ .markdown-editor__content {
435
+ padding: var(--spacing-md) var(--spacing-md);
436
+ outline: none;
437
+ min-height: 200px;
438
+ }
439
+
440
+ .markdown-editor__content > *:first-child {
441
+ margin-top: 0;
442
+ }
443
+
444
+ .markdown-editor__content :deep(h1),
445
+ .markdown-editor__content :deep(h2),
446
+ .markdown-editor__content :deep(h3),
447
+ .markdown-editor__content :deep(h4) {
448
+ margin: var(--spacing-lg) 0 var(--spacing-sm);
449
+ font-weight: 700;
450
+ color: var(--color-text-primary);
451
+ }
452
+
453
+ .markdown-editor__content :deep(h1) {
454
+ font-size: var(--font-size-xl);
455
+ }
456
+ .markdown-editor__content :deep(h2) {
457
+ font-size: var(--font-size-lg);
458
+ }
459
+ .markdown-editor__content :deep(h3) {
460
+ font-size: var(--font-size-base);
461
+ }
462
+ .markdown-editor__content :deep(h4) {
463
+ font-size: var(--font-size-sm);
464
+ text-transform: uppercase;
465
+ letter-spacing: 0.05em;
466
+ color: var(--color-text-secondary);
467
+ }
468
+
469
+ .markdown-editor__content :deep(p) {
470
+ margin: 0 0 var(--spacing-md);
471
+ }
472
+
473
+ .markdown-editor__content :deep(ul),
474
+ .markdown-editor__content :deep(ol) {
475
+ margin: 0 0 var(--spacing-md);
476
+ padding-left: var(--spacing-lg);
477
+ }
478
+
479
+ .markdown-editor__content :deep(li) {
480
+ margin: var(--spacing-xs) 0;
481
+ }
482
+
483
+ .markdown-editor__content :deep(blockquote) {
484
+ margin: var(--spacing-md) 0;
485
+ padding: var(--spacing-sm) var(--spacing-md);
486
+ border-left: 3px solid var(--color-border);
487
+ color: var(--color-text-secondary);
488
+ font-style: italic;
489
+ }
490
+
491
+ .markdown-editor__content :deep(code) {
492
+ font-family: var(--font-family-mono);
493
+ font-size: 0.9em;
494
+ padding: 1px 4px;
495
+ background-color: var(--color-surface);
496
+ border-radius: var(--radius-sm);
497
+ }
498
+
499
+ .markdown-editor__content :deep(pre) {
500
+ margin: var(--spacing-md) 0;
501
+ padding: var(--spacing-md);
502
+ background-color: var(--color-surface);
503
+ border-radius: var(--radius-md);
504
+ overflow-x: auto;
505
+ }
506
+
507
+ .markdown-editor__content :deep(pre code) {
508
+ padding: 0;
509
+ background: none;
510
+ }
511
+
512
+ .markdown-editor__content :deep(a) {
513
+ color: var(--color-primary);
514
+ text-decoration: underline;
515
+ }
516
+
517
+ .markdown-editor__content :deep(hr) {
518
+ border: 0;
519
+ border-top: 1px solid var(--color-border);
520
+ margin: var(--spacing-lg) 0;
521
+ }
522
+
523
+ .markdown-editor__content :deep(strong) {
524
+ font-weight: 700;
525
+ }
526
+
527
+ .markdown-editor__content :deep(table) {
528
+ border-collapse: collapse;
529
+ margin: var(--spacing-md) 0;
530
+ }
531
+
532
+ .markdown-editor__content :deep(th),
533
+ .markdown-editor__content :deep(td) {
534
+ border: 1px solid var(--color-border);
535
+ padding: var(--spacing-xs) var(--spacing-sm);
536
+ }
537
+
538
+ .markdown-editor__content :deep(th) {
539
+ background-color: var(--color-surface);
540
+ font-weight: 600;
541
+ }
542
+ </style>