@linto-ai/transcript-ui-core 0.9.15 → 0.9.16

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linto-ai/transcript-ui-core",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "description": "State layer and UI shell for @linto-ai/transcript-ui — core, stores, format adapters, and the TranscriptUI component",
5
5
  "keywords": [
6
6
  "vue",
@@ -39,8 +39,8 @@
39
39
  "vue": "^3.5.0"
40
40
  },
41
41
  "dependencies": {
42
- "@linto-ai/transcript-ui-i18n": "0.9.15",
43
- "@linto-ai/transcript-ui-ui": "0.9.15",
42
+ "@linto-ai/transcript-ui-i18n": "0.9.16",
43
+ "@linto-ai/transcript-ui-ui": "0.9.16",
44
44
  "vue-stick-to-bottom": "^1.0.0"
45
45
  }
46
46
  }
@@ -12,7 +12,7 @@ import TranslationSelector from "./TranslationSelector.vue"
12
12
  import SelectionActionBar from "./SelectionActionBar.vue"
13
13
  import { useIsMobile } from "../composables/useIsMobile"
14
14
  import { provideTurnSelection } from "../composables/useTurnSelection"
15
- import { useCore } from "../core"
15
+ import { useCore, type LLMService } from "../core"
16
16
 
17
17
  const props = withDefaults(
18
18
  defineProps<{
@@ -27,7 +27,41 @@ const core = useCore()
27
27
  const { isMobile } = useIsMobile()
28
28
  const isSidebarOpen = ref(false)
29
29
 
30
- const activeTab = ref<string>(TRANSCRIPTION_TAB)
30
+ const shownPanels = ref<string[]>([TRANSCRIPTION_TAB])
31
+ // The ?? never actually triggers — every shownPanels mutation keeps at least one entry — it's just satisfying noUncheckedIndexedAccess.
32
+ const activeTab = computed(() => shownPanels.value[0] ?? TRANSCRIPTION_TAB)
33
+
34
+ const isSplit = computed({
35
+ get: () => shownPanels.value.length > 1,
36
+ set: (value: boolean) => {
37
+ shownPanels.value = value
38
+ ? [...shownPanels.value, VERBATIM_TAB]
39
+ : shownPanels.value.filter((id) => id !== VERBATIM_TAB)
40
+ },
41
+ })
42
+
43
+ type PanelDescriptor =
44
+ | { id: string; kind: "transcription" }
45
+ | { id: string; kind: "verbatim" }
46
+ | { id: string; kind: "service"; service: LLMService }
47
+
48
+ // Resolved once per render, not inline in the template: avoids calling
49
+ // core.llmServices.get() twice per item (once to guard, once to bind) with
50
+ // no type-narrowing between the two — a discriminated union the template
51
+ // just switches on instead.
52
+ const panels = computed<PanelDescriptor[]>(() =>
53
+ shownPanels.value.map((id): PanelDescriptor => {
54
+ if (id === TRANSCRIPTION_TAB) return { id, kind: "transcription" }
55
+ if (id === VERBATIM_TAB) return { id, kind: "verbatim" }
56
+ const service = core.llmServices?.get(id)
57
+ // Unknown/vanished service id (e.g. llmServices isn't installed):
58
+ // same fallback as the old single-tab code — show transcription rather
59
+ // than nothing.
60
+ return service
61
+ ? { id, kind: "service", service }
62
+ : { id, kind: "transcription" }
63
+ }),
64
+ )
31
65
 
32
66
  const activeTurns = computed(
33
67
  () => core.activeChannel.value?.activeTranslation.value.turns.value ?? [],
@@ -46,11 +80,6 @@ const activeTranslationId = computed(
46
80
  const speakerList = computed(() => Array.from(speakers.values()))
47
81
 
48
82
  const showTranscription = computed(() => activeTab.value === TRANSCRIPTION_TAB)
49
- const showVerbatim = computed(() => activeTab.value === VERBATIM_TAB)
50
- const activeService = computed(() => {
51
- if (showTranscription.value || showVerbatim.value) return null
52
- return core.llmServices?.get(activeTab.value) ?? null
53
- })
54
83
 
55
84
  watch(activeTab, (id) => {
56
85
  if (!core.llmServices) return
@@ -69,7 +98,7 @@ watch(
69
98
  activeTab.value !== VERBATIM_TAB &&
70
99
  !core.llmServices?.get(activeTab.value)
71
100
  ) {
72
- activeTab.value = TRANSCRIPTION_TAB
101
+ shownPanels.value = [TRANSCRIPTION_TAB]
73
102
  }
74
103
  },
75
104
  )
@@ -115,22 +144,31 @@ function onTranslationChange(translationId: string) {
115
144
  @open-chat="core.chat?.setDrawerOpen(true)"
116
145
  @undo="core.transcriptionEditor?.undo()"
117
146
  @redo="core.transcriptionEditor?.redo()" />
118
- <TabBar v-model="activeTab" />
147
+ <TabBar
148
+ :model-value="activeTab"
149
+ @update:model-value="(tab) => (shownPanels = [tab])" />
119
150
  <SelectionActionBar v-if="showTranscription" />
120
- <main class="editor-body">
121
- <TranscriptionPanel
122
- v-if="showTranscription"
123
- :turns="activeTurns"
124
- :speakers="speakers" />
125
- <VerbatimPanel v-else-if="showVerbatim" />
126
- <component
127
- :is="core.components.llmServicePanel"
128
- v-else-if="activeService"
129
- :key="activeService.id"
130
- :service="activeService" />
131
- <TranscriptionPanel v-else :turns="activeTurns" :speakers="speakers" />
151
+ <main
152
+ class="editor-body"
153
+ :class="{ 'editor-body--no-sidebar': panels.length > 1 }">
154
+ <div
155
+ class="editor-body__panels"
156
+ :class="{ 'editor-body__panels--split': panels.length > 1 }">
157
+ <template v-for="panel in panels" :key="panel.id">
158
+ <TranscriptionPanel
159
+ v-if="panel.kind === 'transcription'"
160
+ :turns="activeTurns"
161
+ :speakers="speakers" />
162
+ <VerbatimPanel v-else-if="panel.kind === 'verbatim'" />
163
+ <component
164
+ :is="core.components.llmServicePanel"
165
+ v-else
166
+ :service="panel.service"
167
+ v-model:split="isSplit" />
168
+ </template>
169
+ </div>
132
170
  <SpeakerSidebar
133
- v-if="!isMobile"
171
+ v-if="!isMobile && panels.length === 1"
134
172
  :speakers="speakerList"
135
173
  :channels="channels"
136
174
  :selected-channel-id="core.activeChannelId.value"
@@ -140,7 +178,9 @@ function onTranslationChange(translationId: string) {
140
178
  @update:selected-channel-id="onChannelChange"
141
179
  @update:selected-translation-id="onTranslationChange" />
142
180
 
143
- <SidebarDrawer v-if="isMobile" v-model:open="isSidebarOpen">
181
+ <SidebarDrawer
182
+ v-if="isMobile && panels.length === 1"
183
+ v-model:open="isSidebarOpen">
144
184
  <SpeakerSidebar
145
185
  :speakers="speakerList"
146
186
  :channels="channels"
@@ -201,6 +241,27 @@ function onTranslationChange(translationId: string) {
201
241
  min-height: 0;
202
242
  }
203
243
 
244
+ /* Split mode: two panels already share the body between them, no room (or
245
+ need) for the speaker sidebar too — see Layout's panels/isSplit state. */
246
+ .editor-body--no-sidebar {
247
+ grid-template-columns: 1fr;
248
+ }
249
+
250
+ .editor-body__panels {
251
+ display: flex;
252
+ min-width: 0;
253
+ min-height: 0;
254
+ }
255
+
256
+ .editor-body__panels > * {
257
+ flex: 1;
258
+ min-width: 0;
259
+ }
260
+
261
+ .editor-body__panels--split > * + * {
262
+ border-left: 1px solid var(--color-border);
263
+ }
264
+
204
265
  .mobile-selectors {
205
266
  display: flex;
206
267
  gap: var(--spacing-sm);
@@ -55,7 +55,13 @@ defineExpose({ core })
55
55
  </template>
56
56
 
57
57
  <style lang="css">
58
- @import "@linto-ai/transcript-ui-ui/styles/fonts.css";
58
+ /* NOT fonts.css: a @font-face declared inside a shadow root isn't reliably
59
+ * applied by browsers (document.fonts never registers it) — dead weight in
60
+ * the webcomponent bundle specifically, which always runs shadow-DOM'd. The
61
+ * host declares --font-family's actual font at document level instead (see
62
+ * variables.css's token doc). A direct, non-shadow-DOM Vue embedder of this
63
+ * package (no such limitation) can still opt in: `@import
64
+ * "@linto-ai/transcript-ui-ui/styles/fonts.css"` themselves. */
59
65
  @import "@linto-ai/transcript-ui-ui/styles/variables.css";
60
66
  @import "@linto-ai/transcript-ui-ui/styles/base.css";
61
67
  @import "@linto-ai/transcript-ui-ui/styles/popover-list.css";
@@ -111,7 +111,7 @@ function onExport(format?: string): void {
111
111
  }
112
112
 
113
113
  .verbatim-panel__content {
114
- padding: 4rem 6rem;
114
+ padding: 4rem clamp(1.5rem, 6rem, 8%);
115
115
  }
116
116
 
117
117
  .verbatim-panel__header {
@@ -167,9 +167,4 @@ function onExport(format?: string): void {
167
167
  color: var(--color-text-primary);
168
168
  }
169
169
 
170
- @media (max-width: 767px) {
171
- .verbatim-panel {
172
- padding: var(--spacing-md);
173
- }
174
- }
175
170
  </style>