@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/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ // ── Atoms ──────────────────────────────────────────────────────────────
2
+ export { default as Badge } from './atoms/Badge.vue'
3
+ export { default as Button } from './atoms/Button.vue'
4
+ export { default as CodeBlock } from './atoms/CodeBlock.vue'
5
+ export { default as CopyButton } from './atoms/CopyButton.vue'
6
+ export { default as EditableText } from './atoms/EditableText.vue'
7
+ export { default as EditorCheckbox } from './atoms/EditorCheckbox.vue'
8
+ export { default as EditorIcon } from './atoms/EditorIcon.vue'
9
+ export { default as MarkdownEditor } from './atoms/MarkdownEditor.vue'
10
+ export { default as MarkdownView } from './atoms/MarkdownView.vue'
11
+ export { default as PopoverList } from './atoms/PopoverList.vue'
12
+ export { default as SelectableListItem } from './atoms/SelectableListItem.vue'
13
+ export { default as SpeakerIndicator } from './atoms/SpeakerIndicator.vue'
14
+ export { default as SwitchToggle } from './atoms/SwitchToggle.vue'
15
+ export { default as UserAvatar } from './atoms/UserAvatar.vue'
16
+ export { resolveIcon, iconMap, ICON_SIZES } from './atoms/icons'
17
+
18
+ // ── Molecules ──────────────────────────────────────────────────────────
19
+ export { default as DocumentArticle } from './molecules/DocumentArticle.vue'
20
+ export type { DocumentArticleStatus } from './molecules/DocumentArticle.vue'
21
+ export { default as DownloadMenu } from './molecules/DownloadMenu.vue'
22
+ export type { DownloadFormat } from './molecules/DownloadMenu.vue'
23
+ export { default as FormInput, EMPTY_FIELD } from './molecules/FormInput.vue'
24
+ export type { FormField } from './molecules/FormInput.vue'
25
+ export { default as SpeakerMenu } from './molecules/SpeakerMenu.vue'
26
+ export { default as Tabs } from './molecules/Tabs.vue'
27
+ export type { TabItem } from './molecules/Tabs.vue'
28
+ export { default as TurnTextEditor } from './molecules/TurnTextEditor.vue'
29
+
30
+ // ── Utils ──────────────────────────────────────────────────────────────
31
+ export { renderMarkdown, renderMarkdownSegments } from './utils/markdown'
32
+ export { computeInitials } from './utils/computeInitials'
33
+ export { placeCaretAt } from './utils/placeCaretAt'
34
+ export { computeSelectionOffset } from './utils/computeSelectionOffset'
35
+ export { computeTextOffsetInContainer } from './utils/computeTextOffsetInContainer'
36
+ export { getShadowAwareSelection, getSelectionFocus } from './utils/shadowAwareSelection'
@@ -0,0 +1,169 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue"
3
+ import EditorIcon from "../atoms/EditorIcon.vue"
4
+ import Button from "../atoms/Button.vue"
5
+ import { useI18n } from "@linto-ai/transcript-ui-i18n"
6
+
7
+ export type DocumentArticleStatus = "done" | "processing" | "error"
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ status?: DocumentArticleStatus
12
+ progress?: number
13
+ errorMessage?: string
14
+ }>(),
15
+ { status: "done" },
16
+ )
17
+
18
+ const emit = defineEmits<{
19
+ retry: []
20
+ }>()
21
+
22
+ const { t } = useI18n()
23
+
24
+ const errorText = computed(
25
+ () => props.errorMessage || t("llmService.errorTemporary"),
26
+ )
27
+
28
+ const progressValue = computed(() => {
29
+ const v = props.progress
30
+ if (v == null || !Number.isFinite(v)) return null
31
+ return Math.max(0, Math.min(100, Math.round(v)))
32
+ })
33
+ </script>
34
+
35
+ <template>
36
+ <article class="document-article" :data-status="props.status">
37
+ <div
38
+ class="document-article__toolbar"
39
+ role="toolbar"
40
+ v-if="
41
+ $slots['toolbar-left'] ||
42
+ $slots['toolbar-center'] ||
43
+ $slots['toolbar-right']
44
+ ">
45
+ <div class="document-article__toolbar-left">
46
+ <slot name="toolbar-left" />
47
+ </div>
48
+ <div class="document-article__toolbar-center">
49
+ <slot name="toolbar-center" />
50
+ </div>
51
+ <div class="document-article__toolbar-right">
52
+ <slot name="toolbar-right" />
53
+ </div>
54
+ </div>
55
+
56
+ <div class="document-article__body">
57
+ <div
58
+ v-if="props.status === 'processing'"
59
+ class="document-article__center document-article__center--processing"
60
+ role="status"
61
+ aria-live="polite">
62
+ <EditorIcon name="spinner" spin :size="24" />
63
+ <progress
64
+ class="document-article__progress"
65
+ :max="100"
66
+ :value="progressValue ?? undefined" />
67
+ <span
68
+ v-if="progressValue !== null"
69
+ class="document-article__progress-value">
70
+ {{ progressValue }}%
71
+ </span>
72
+ </div>
73
+ <div
74
+ v-else-if="props.status === 'error'"
75
+ class="document-article__center document-article__center--error"
76
+ role="alert">
77
+ <p class="document-article__error-text">{{ errorText }}</p>
78
+ <Button variant="primary" icon="refresh-cw" @click="emit('retry')">
79
+ {{ t("llmService.retry") }}
80
+ </Button>
81
+ </div>
82
+ <slot v-else />
83
+ </div>
84
+ </article>
85
+ </template>
86
+
87
+ <style scoped>
88
+ .document-article {
89
+ width: 100%;
90
+ max-width: 760px;
91
+ margin: var(--spacing-lg) auto;
92
+ background-color: var(--color-surface);
93
+ border: 1px solid var(--color-border);
94
+ border-radius: var(--radius-md);
95
+ display: flex;
96
+ flex-direction: column;
97
+ }
98
+
99
+ .document-article__toolbar {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: var(--spacing-sm);
103
+ padding: var(--spacing-sm) var(--spacing-md);
104
+ border-bottom: 1px solid var(--color-border);
105
+ position: sticky;
106
+ top: 0;
107
+ background-color: var(--color-surface);
108
+ border-radius: var(--radius-md) var(--radius-md) 0 0;
109
+ z-index: 1;
110
+ }
111
+
112
+ .document-article__toolbar-left,
113
+ .document-article__toolbar-right {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: var(--spacing-xs);
117
+ flex-shrink: 0;
118
+ }
119
+
120
+ .document-article__toolbar-center {
121
+ flex: 1;
122
+ display: flex;
123
+ justify-content: center;
124
+ align-items: center;
125
+ min-width: 0;
126
+ }
127
+
128
+ .document-article__body {
129
+ flex: 1;
130
+ min-height: 0;
131
+ }
132
+
133
+ .document-article__center {
134
+ display: flex;
135
+ flex-direction: column;
136
+ align-items: center;
137
+ justify-content: center;
138
+ gap: var(--spacing-sm);
139
+ padding: var(--spacing-xl) var(--spacing-md);
140
+ text-align: center;
141
+ }
142
+
143
+ .document-article__center--processing {
144
+ color: var(--color-primary);
145
+ }
146
+
147
+ .document-article__center--error {
148
+ color: var(--color-danger, #d33);
149
+ }
150
+
151
+ .document-article__progress {
152
+ width: min(280px, 100%);
153
+ height: 6px;
154
+ }
155
+
156
+ .document-article__progress-value {
157
+ font-size: var(--font-size-xs);
158
+ font-variant-numeric: tabular-nums;
159
+ color: var(--color-text-muted);
160
+ }
161
+
162
+ .document-article__error-text {
163
+ margin: 0;
164
+ max-width: 480px;
165
+ font-size: var(--font-size-sm);
166
+ line-height: var(--line-height);
167
+ color: var(--color-text-secondary);
168
+ }
169
+ </style>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import PopoverList from "../atoms/PopoverList.vue"
3
+ import Button from "../atoms/Button.vue"
4
+ import { useI18n, type TranslationKey } from "@linto-ai/transcript-ui-i18n"
5
+
6
+ export interface DownloadFormat {
7
+ format: string
8
+ labelKey: TranslationKey
9
+ }
10
+
11
+ const props = defineProps<{
12
+ formats: DownloadFormat[]
13
+ disabled?: boolean
14
+ loading?: boolean
15
+ }>()
16
+
17
+ const emit = defineEmits<{
18
+ select: [format: string]
19
+ }>()
20
+
21
+ const { t } = useI18n()
22
+
23
+ function onSelect(item: DownloadFormat): void {
24
+ emit("select", item.format)
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <PopoverList
30
+ :items="props.formats"
31
+ :item-key="(f: DownloadFormat) => f.format"
32
+ align="end"
33
+ @select="onSelect">
34
+ <template #trigger>
35
+ <Button
36
+ variant="primary"
37
+ icon="download"
38
+ icon-right="chevron-down"
39
+ :disabled="disabled"
40
+ :loading="loading">
41
+ {{ t("llmService.download") }}
42
+ </Button>
43
+ </template>
44
+ <template #item="{ item }">
45
+ <span>{{ t(item.labelKey) }}</span>
46
+ </template>
47
+ </PopoverList>
48
+ </template>