@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
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@linto-ai/transcript-ui-ui",
3
+ "version": "0.9.0",
4
+ "description": "Design-system components and CSS tokens for @linto-ai/transcript-ui",
5
+ "keywords": [
6
+ "vue",
7
+ "vue3",
8
+ "design-system",
9
+ "ui-kit",
10
+ "linto"
11
+ ],
12
+ "sideEffects": [
13
+ "**/*.vue",
14
+ "**/*.css"
15
+ ],
16
+ "homepage": "https://github.com/linto-ai/linto-studio",
17
+ "bugs": "https://github.com/linto-ai/linto-studio/issues",
18
+ "author": "Tom Darboux <tdarboux@linagora.com>",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/linto-ai/linto-studio.git",
22
+ "directory": "studio-sdk/components/transcript-ui/packages/ui"
23
+ },
24
+ "type": "module",
25
+ "license": "AGPL-3.0",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "files": [
30
+ "src"
31
+ ],
32
+ "main": "./src/index.ts",
33
+ "types": "./src/index.ts",
34
+ "exports": {
35
+ ".": "./src/index.ts",
36
+ "./styles/*": "./src/styles/*"
37
+ },
38
+ "peerDependencies": {
39
+ "vue": "^3.5.0"
40
+ },
41
+ "dependencies": {
42
+ "@linto-ai/transcript-ui-i18n": "0.9.0",
43
+ "dompurify": "^3.2.4",
44
+ "lucide-vue-next": "^0.564.0",
45
+ "marked": "^18.0.2",
46
+ "prismjs": "^1.30.0",
47
+ "reka-ui": "^2.8.0",
48
+ "turndown": "^7.2.4",
49
+ "turndown-plugin-gfm": "^1.0.2"
50
+ },
51
+ "devDependencies": {
52
+ "@types/prismjs": "^1.26.6",
53
+ "@types/turndown": "^5.0.6"
54
+ }
55
+ }
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ ariaLabel?: string
4
+ }>()
5
+ </script>
6
+
7
+ <template>
8
+ <span class="editor-badge" :aria-label="ariaLabel">
9
+ <slot />
10
+ </span>
11
+ </template>
12
+
13
+ <style scoped>
14
+ .editor-badge {
15
+ display: inline-flex;
16
+ align-items: center;
17
+ padding: 2px var(--spacing-sm);
18
+ font-size: var(--font-size-xs);
19
+ font-weight: 500;
20
+ color: var(--color-text-muted);
21
+ background-color: var(--color-surface);
22
+ border: 1px solid var(--color-border-light);
23
+ border-radius: var(--radius-sm);
24
+ white-space: nowrap;
25
+ }
26
+ </style>
@@ -0,0 +1,264 @@
1
+ <script setup lang="ts">
2
+ import { computed, useSlots } from "vue"
3
+ import EditorIcon from "./EditorIcon.vue"
4
+ import { resolveIcon, ICON_SIZES } from "./icons"
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ label?: string
9
+ icon?: string
10
+ iconRight?: string
11
+ variant?: "primary" | "secondary" | "tertiary" | "transparent" | "inverse"
12
+ intent?: "default" | "destructive"
13
+ size?: "sm" | "md" | "lg"
14
+ disabled?: boolean
15
+ loading?: boolean
16
+ block?: boolean
17
+ type?: "button" | "submit"
18
+ ariaLabel?: string
19
+ }>(),
20
+ {
21
+ variant: "tertiary",
22
+ intent: "default",
23
+ size: "sm",
24
+ disabled: false,
25
+ loading: false,
26
+ block: false,
27
+ type: "button",
28
+ },
29
+ )
30
+
31
+ const slots = useSlots()
32
+
33
+ const hasResolvedIcon = computed(() => !!resolveIcon(props.icon))
34
+ const hasResolvedIconRight = computed(() => !!resolveIcon(props.iconRight))
35
+ const iconSize = computed(() => ICON_SIZES[props.size])
36
+
37
+ const isDisabled = computed(() => props.disabled || props.loading)
38
+ const hasLabel = computed(() => !!props.label || !!slots.default)
39
+ const hasLeftIcon = computed(
40
+ () => props.loading || hasResolvedIcon.value || !!slots.icon,
41
+ )
42
+ const isIconOnly = computed(() => hasLeftIcon.value && !hasLabel.value)
43
+
44
+ const classes = computed(() => [
45
+ "editor-btn",
46
+ `editor-btn--${props.variant}`,
47
+ `editor-btn--${props.intent}`,
48
+ `editor-btn--${props.size}`,
49
+ isIconOnly.value && "editor-btn--icon-only",
50
+ props.block && "editor-btn--block",
51
+ ])
52
+ </script>
53
+
54
+ <template>
55
+ <button
56
+ :type="type"
57
+ :class="classes"
58
+ :disabled="isDisabled"
59
+ :aria-disabled="isDisabled"
60
+ :aria-label="ariaLabel">
61
+ <EditorIcon v-if="loading" name="spinner" spin :size="iconSize" />
62
+ <EditorIcon v-else-if="hasResolvedIcon" :name="icon!" :size="iconSize" />
63
+ <slot v-else-if="$slots.icon" name="icon" />
64
+ <span v-if="hasLabel" class="editor-btn__label">
65
+ <slot>{{ label }}</slot>
66
+ </span>
67
+ <EditorIcon
68
+ v-if="hasResolvedIconRight"
69
+ :name="iconRight!"
70
+ :size="iconSize" />
71
+ <slot v-else-if="$slots['icon-right']" name="icon-right" />
72
+ </button>
73
+ </template>
74
+
75
+ <style scoped>
76
+ .editor-btn {
77
+ /* Default tokens — overridden by variant/intent/size modifiers */
78
+ --btn-bg: transparent;
79
+ --btn-text: var(--color-text-secondary);
80
+ --btn-border-color: var(--color-border);
81
+ --btn-hover-bg: var(--color-surface-hover);
82
+ --btn-hover-text: var(--color-text-primary);
83
+ --btn-padding-y: 0;
84
+ --btn-padding-x: var(--spacing-sm);
85
+ --btn-font-size: var(--font-size-xs);
86
+ --btn-height: 32px;
87
+ --btn-gap: var(--spacing-xs);
88
+
89
+ display: inline-flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ gap: var(--btn-gap);
93
+ box-sizing: border-box;
94
+ height: var(--btn-height);
95
+ padding: var(--btn-padding-y) var(--btn-padding-x);
96
+ font-family: var(--font-family);
97
+ font-size: var(--btn-font-size);
98
+ font-weight: 500;
99
+ line-height: 1;
100
+ color: var(--btn-text);
101
+ background-color: var(--btn-bg);
102
+ border: 1px solid var(--btn-border-color);
103
+ border-radius: var(--radius-sm);
104
+ cursor: pointer;
105
+ white-space: nowrap;
106
+ transition:
107
+ background-color var(--transition-duration),
108
+ color var(--transition-duration),
109
+ border-color var(--transition-duration);
110
+ }
111
+
112
+ .editor-btn:hover:not(:disabled) {
113
+ background-color: var(--btn-hover-bg);
114
+ color: var(--btn-hover-text);
115
+ }
116
+
117
+ .editor-btn:focus-visible {
118
+ outline: 2px solid var(--color-primary);
119
+ outline-offset: 2px;
120
+ }
121
+
122
+ /* Note: this rule is repeated lower in the file (after variants) to win the
123
+ cascade on the variant CSS vars. Keep this lightweight version for the
124
+ cursor and hover suppression. */
125
+ .editor-btn:disabled {
126
+ cursor: not-allowed;
127
+ }
128
+
129
+ .editor-btn:disabled:hover {
130
+ background-color: var(--btn-bg);
131
+ color: var(--btn-text);
132
+ }
133
+
134
+ .editor-btn__label {
135
+ /* //overflow: hidden;
136
+ text-overflow: ellipsis; */
137
+ text-overflow: ellipsis;
138
+ text-box: cap alphabetic;
139
+ }
140
+
141
+ /* Sizes */
142
+ .editor-btn--sm {
143
+ /* defaults */
144
+ }
145
+
146
+ .editor-btn--md {
147
+ --btn-padding-y: 0;
148
+ --btn-padding-x: var(--spacing-md);
149
+ --btn-font-size: var(--font-size-sm);
150
+ --btn-height: 40px;
151
+ }
152
+
153
+ .editor-btn--lg {
154
+ --btn-padding-y: 0;
155
+ --btn-padding-x: var(--spacing-md);
156
+ --btn-font-size: var(--font-size-base);
157
+ --btn-height: 44px;
158
+ }
159
+
160
+ /* Icon-only: square */
161
+ .editor-btn--icon-only {
162
+ width: var(--btn-height);
163
+ padding: 0;
164
+ }
165
+
166
+ .editor-btn--block {
167
+ display: flex;
168
+ width: 100%;
169
+ }
170
+
171
+ /* Variants — default intent */
172
+ .editor-btn--primary {
173
+ --btn-bg: var(--color-primary);
174
+ --btn-text: var(--color-white);
175
+ --btn-border-color: var(--color-primary);
176
+ --btn-hover-bg: var(--color-primary-hover);
177
+ --btn-hover-text: var(--color-white);
178
+ }
179
+
180
+ .editor-btn--secondary {
181
+ --btn-bg: transparent;
182
+ --btn-text: var(--color-primary);
183
+ --btn-border-color: var(--color-primary);
184
+ --btn-hover-bg: var(--color-primary);
185
+ --btn-hover-text: var(--color-white);
186
+ }
187
+
188
+ .editor-btn--tertiary {
189
+ --btn-bg: transparent;
190
+ --btn-text: var(--color-text-primary);
191
+ --btn-border-color: var(--color-border);
192
+ --btn-hover-bg: var(--color-surface-hover);
193
+ --btn-hover-text: var(--color-text-primary);
194
+ }
195
+
196
+ .editor-btn--transparent {
197
+ --btn-bg: transparent;
198
+ --btn-text: var(--color-text-secondary);
199
+ --btn-border-color: transparent;
200
+ --btn-hover-bg: var(--color-surface-hover);
201
+ --btn-hover-text: var(--color-text-primary);
202
+ }
203
+
204
+ /* Literal theme inversion: background = the theme's text color and
205
+ vice-versa — high contrast in both light and dark themes without
206
+ borrowing the primary color's semantics. */
207
+ .editor-btn--inverse {
208
+ --btn-bg: var(--color-text-primary);
209
+ --btn-text: var(--color-background);
210
+ --btn-border-color: transparent;
211
+ --btn-hover-bg: var(--color-text-secondary);
212
+ --btn-hover-text: var(--color-background);
213
+ }
214
+
215
+ /* Destructive intent overrides */
216
+ .editor-btn--destructive.editor-btn--primary {
217
+ --btn-bg: var(--color-danger);
218
+ --btn-text: var(--color-white);
219
+ --btn-border-color: var(--color-danger);
220
+ --btn-hover-bg: var(--color-danger-hover);
221
+ --btn-hover-text: var(--color-white);
222
+ }
223
+
224
+ .editor-btn--destructive.editor-btn--secondary {
225
+ --btn-bg: transparent;
226
+ --btn-text: var(--color-danger);
227
+ --btn-border-color: var(--color-danger);
228
+ --btn-hover-bg: var(--color-danger);
229
+ --btn-hover-text: var(--color-white);
230
+ }
231
+
232
+ .editor-btn--destructive.editor-btn--tertiary,
233
+ .editor-btn--destructive.editor-btn--transparent {
234
+ --btn-text: var(--color-danger);
235
+ --btn-hover-bg: var(--color-danger-soft);
236
+ --btn-hover-text: var(--color-danger);
237
+ }
238
+
239
+ /* Disabled: gray-out regardless of variant. Placed after the variants so the
240
+ CSS var overrides win the cascade (same specificity, last declaration). */
241
+ .editor-btn:disabled {
242
+ --btn-bg: var(--color-surface);
243
+ --btn-text: var(--color-text-muted);
244
+ --btn-border-color: var(--color-border);
245
+ --btn-hover-bg: var(--color-surface);
246
+ --btn-hover-text: var(--color-text-muted);
247
+ }
248
+
249
+ /* The transparent variant has no chrome when enabled — disabling it must not
250
+ ADD a box; the muted text alone carries the disabled signal. */
251
+ .editor-btn--transparent:disabled {
252
+ --btn-bg: transparent;
253
+ --btn-border-color: transparent;
254
+ --btn-hover-bg: transparent;
255
+ }
256
+
257
+ /* Inverse stays a filled, borderless chip when disabled — dimmed, but the
258
+ silhouette must not change. */
259
+ .editor-btn--inverse:disabled {
260
+ --btn-bg: var(--color-surface-hover);
261
+ --btn-border-color: transparent;
262
+ --btn-hover-bg: var(--color-surface-hover);
263
+ }
264
+ </style>
@@ -0,0 +1,98 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from "vue"
3
+ import CopyButton from "./CopyButton.vue"
4
+ import { useI18n } from "@linto-ai/transcript-ui-i18n"
5
+
6
+ const props = defineProps<{
7
+ code: string
8
+ lang?: string
9
+ streaming?: boolean
10
+ }>()
11
+
12
+ const { t } = useI18n()
13
+
14
+ function copy() {
15
+ return navigator.clipboard.writeText(props.code)
16
+ }
17
+
18
+ // Highlighted token markup, or null to render plain text (streaming, unknown
19
+ // language, or before the lazy highlighter chunk has loaded).
20
+ const highlighted = ref<string | null>(null)
21
+ let seq = 0
22
+
23
+ watch(
24
+ () => [props.code, props.lang, props.streaming] as const,
25
+ async ([code, lang, streaming]) => {
26
+ const run = ++seq
27
+ if (streaming || !code) {
28
+ highlighted.value = null
29
+ return
30
+ }
31
+ const { highlightCode } = await import("../utils/highlight")
32
+ // Drop the result if another change superseded this run while awaiting.
33
+ if (run === seq) highlighted.value = highlightCode(code, lang ?? "")
34
+ },
35
+ { immediate: true },
36
+ )
37
+ </script>
38
+
39
+ <template>
40
+ <div class="code-block">
41
+ <CopyButton
42
+ v-if="!streaming"
43
+ class="code-block__copy"
44
+ variant="transparent"
45
+ size="sm"
46
+ :copy-fn="copy"
47
+ :aria-label="t('markdown.copyCode')" />
48
+ <pre><code v-if="highlighted" v-html="highlighted" /><code v-else>{{ code }}</code></pre>
49
+ </div>
50
+ </template>
51
+
52
+ <style scoped>
53
+ .code-block {
54
+ position: relative;
55
+ }
56
+
57
+ .code-block__copy {
58
+ position: absolute;
59
+ top: var(--spacing-xs);
60
+ right: var(--spacing-xs);
61
+ opacity: 0;
62
+ transition: opacity 0.15s ease;
63
+ }
64
+
65
+ .code-block:hover .code-block__copy,
66
+ .code-block:focus-within .code-block__copy {
67
+ opacity: 1;
68
+ }
69
+
70
+ .code-block pre {
71
+ margin: var(--spacing-md) 0;
72
+ padding: var(--spacing-md);
73
+ background-color: var(--color-surface);
74
+ border-radius: var(--radius-md);
75
+ overflow-x: auto;
76
+ border: 1px solid var(--color-border);
77
+ }
78
+
79
+ .code-block pre code {
80
+ padding: 0;
81
+ background: none;
82
+ font-family: var(--font-family-mono);
83
+ font-size: 0.9em;
84
+ }
85
+
86
+ @media (prefers-reduced-motion: reduce) {
87
+ .code-block__copy {
88
+ transition: none;
89
+ }
90
+ }
91
+ </style>
92
+
93
+ <!-- Not scoped: Prism generates plain, unscoped `.token.*` spans via
94
+ innerHTML, so a scoped style's data-v-xxx attribute selector would never
95
+ match them. -->
96
+ <style>
97
+ @import "prismjs/themes/prism.css";
98
+ </style>
@@ -0,0 +1,98 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from "vue"
3
+ import Button from "./Button.vue"
4
+ import EditorIcon from "./EditorIcon.vue"
5
+ import { ICON_SIZES } from "./icons"
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ icon?: "copy" | "clipboard-list" | "clipboard-type"
10
+ copyFn: () => Promise<void>
11
+ variant?: "primary" | "secondary" | "tertiary" | "transparent"
12
+ size?: "sm" | "md" | "lg"
13
+ disabled?: boolean
14
+ block?: boolean
15
+ ariaLabel?: string
16
+ }>(),
17
+ {
18
+ icon: "copy",
19
+ },
20
+ )
21
+
22
+ const copied = ref(false)
23
+ let timer: ReturnType<typeof setTimeout> | undefined
24
+
25
+ async function onClick() {
26
+ if (copied.value) return
27
+ try {
28
+ await props.copyFn()
29
+ copied.value = true
30
+ timer = setTimeout(() => {
31
+ copied.value = false
32
+ }, 2000)
33
+ } catch (e) {
34
+ console.error(e)
35
+ }
36
+ }
37
+
38
+ defineExpose({
39
+ reset: () => {
40
+ copied.value = false
41
+ clearTimeout(timer)
42
+ },
43
+ })
44
+
45
+ const currentIconName = computed(() => (copied.value ? "check" : props.icon))
46
+ const iconSize = computed(() => ICON_SIZES[props.size ?? "sm"])
47
+ </script>
48
+
49
+ <template>
50
+ <Button
51
+ :variant="variant"
52
+ :size="size"
53
+ :disabled="disabled"
54
+ :block="block"
55
+ :aria-label="ariaLabel"
56
+ :class="{ 'copy-btn--copied': copied }"
57
+ @click="onClick">
58
+ <template #icon>
59
+ <Transition name="copy-icon" mode="out-in">
60
+ <EditorIcon
61
+ :key="currentIconName"
62
+ :name="currentIconName"
63
+ :size="iconSize" />
64
+ </Transition>
65
+ </template>
66
+ <slot />
67
+ </Button>
68
+ </template>
69
+
70
+ <style scoped>
71
+ .copy-btn--copied {
72
+ color: var(--color-success, #2e7d32);
73
+ }
74
+
75
+ .copy-icon-enter-active,
76
+ .copy-icon-leave-active {
77
+ transition:
78
+ opacity var(--transition-duration) ease,
79
+ scale var(--transition-duration) ease;
80
+ }
81
+
82
+ .copy-icon-enter-from {
83
+ opacity: 0;
84
+ scale: 0.6;
85
+ }
86
+
87
+ .copy-icon-leave-to {
88
+ opacity: 0;
89
+ scale: 0.6;
90
+ }
91
+
92
+ @media (prefers-reduced-motion: reduce) {
93
+ .copy-icon-enter-active,
94
+ .copy-icon-leave-active {
95
+ transition: none;
96
+ }
97
+ }
98
+ </style>
@@ -0,0 +1,115 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, nextTick, useTemplateRef, computed } from "vue"
3
+ import FormInput, { type FormField } from "../molecules/FormInput.vue"
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ modelValue: string
8
+ disabled?: boolean
9
+ placeholder?: string
10
+ ariaLabel?: string
11
+ }>(),
12
+ { disabled: false },
13
+ )
14
+
15
+ const emit = defineEmits<{
16
+ "update:modelValue": [value: string]
17
+ commit: [value: string]
18
+ cancel: []
19
+ }>()
20
+
21
+ const isEditing = ref(false)
22
+ const draft = ref(props.modelValue)
23
+ const inputRef = useTemplateRef<{ focus: () => void; select: () => void }>("input")
24
+
25
+ const field = computed<FormField>(() => ({
26
+ placeholder: props.placeholder,
27
+ customParams: props.ariaLabel ? { "aria-label": props.ariaLabel } : undefined,
28
+ }))
29
+
30
+ watch(
31
+ () => props.modelValue,
32
+ (v) => {
33
+ if (!isEditing.value) draft.value = v
34
+ },
35
+ )
36
+
37
+ async function startEdit(): Promise<void> {
38
+ if (props.disabled) return
39
+ draft.value = props.modelValue
40
+ isEditing.value = true
41
+ await nextTick()
42
+ inputRef.value?.focus()
43
+ inputRef.value?.select()
44
+ }
45
+
46
+ function commit(): void {
47
+ if (!isEditing.value) return
48
+ const trimmed = draft.value.trim()
49
+ isEditing.value = false
50
+ if (!trimmed || trimmed === props.modelValue) return
51
+ emit("update:modelValue", trimmed)
52
+ emit("commit", trimmed)
53
+ }
54
+
55
+ function cancel(): void {
56
+ if (!isEditing.value) return
57
+ isEditing.value = false
58
+ draft.value = props.modelValue
59
+ emit("cancel")
60
+ }
61
+
62
+ function onKeydown(e: KeyboardEvent): void {
63
+ if (e.key === "Enter") {
64
+ e.preventDefault()
65
+ commit()
66
+ } else if (e.key === "Escape") {
67
+ e.preventDefault()
68
+ cancel()
69
+ }
70
+ }
71
+ </script>
72
+
73
+ <template>
74
+ <FormInput
75
+ v-if="isEditing"
76
+ ref="input"
77
+ v-model="draft"
78
+ :field="field"
79
+ size="sm"
80
+ full-width
81
+ @keydown="onKeydown"
82
+ @blur="commit" />
83
+ <button
84
+ v-else
85
+ type="button"
86
+ class="editable-text-display"
87
+ :disabled="disabled"
88
+ :aria-label="ariaLabel"
89
+ @click="startEdit">
90
+ {{ modelValue || placeholder }}
91
+ </button>
92
+ </template>
93
+
94
+ <style scoped>
95
+ .editable-text-display {
96
+ all: unset;
97
+ cursor: text;
98
+ text-align: left;
99
+ font: inherit;
100
+ color: inherit;
101
+ line-height: inherit;
102
+ padding: 0;
103
+ border: 1px solid transparent;
104
+ border-radius: var(--radius-sm);
105
+ min-width: 0;
106
+ }
107
+
108
+ .editable-text-display:not(:disabled):hover {
109
+ border-color: var(--color-border);
110
+ }
111
+
112
+ .editable-text-display:disabled {
113
+ cursor: default;
114
+ }
115
+ </style>