@linto-ai/transcript-ui-core 0.9.18 → 0.9.19

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.18",
3
+ "version": "0.9.19",
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.18",
43
- "@linto-ai/transcript-ui-ui": "0.9.18",
42
+ "@linto-ai/transcript-ui-i18n": "0.9.19",
43
+ "@linto-ai/transcript-ui-ui": "0.9.19",
44
44
  "vue-stick-to-bottom": "^1.0.0"
45
45
  }
46
46
  }
@@ -176,15 +176,16 @@ onBeforeUnmount(() => {
176
176
  </div>
177
177
 
178
178
  <Transition name="fade-slide">
179
- <Button
180
- v-if="showResumeButton"
181
- size="sm"
182
- icon="arrow-down"
183
- class="resume-scroll-btn"
184
- :aria-label="t('transcription.resumeScroll')"
185
- @click="onResumeClick">
186
- {{ t("transcription.resumeScroll") }}
187
- </Button>
179
+ <div v-if="showResumeButton" class="resume-scroll-anchor">
180
+ <Button
181
+ size="sm"
182
+ icon="arrow-down"
183
+ class="resume-scroll-btn"
184
+ :aria-label="t('transcription.resumeScroll')"
185
+ @click="onResumeClick">
186
+ {{ t("transcription.resumeScroll") }}
187
+ </Button>
188
+ </div>
188
189
  </Transition>
189
190
  </div>
190
191
  </article>
@@ -231,13 +232,22 @@ onBeforeUnmount(() => {
231
232
  font-size: var(--font-size-sm);
232
233
  }
233
234
 
234
- /* Resume scroll button */
235
- .resume-scroll-btn {
235
+ /* Resume scroll button — placement lives on this wrapper, never on the
236
+ button itself: Button.vue's `.editor-btn` base rule resets `position` via
237
+ `all: unset` (and, in the webcomponent build, gets re-injected a second
238
+ time after everything else to reach teleported popovers/dialogs — see
239
+ packages/webcomponent/src/index.ts), so any positioning put directly on
240
+ the button is one rebuild away from being silently overridden again. */
241
+ .resume-scroll-anchor {
236
242
  position: sticky;
237
243
  bottom: var(--spacing-lg);
238
- left: 50%;
239
- translate: -50% 0;
240
244
  z-index: var(--z-sticky);
245
+ display: flex;
246
+ flex-direction: column;
247
+ align-items: center;
248
+ }
249
+
250
+ .resume-scroll-btn {
241
251
  /* No backdrop-filter: this button is sticky inside the tall scroll
242
252
  container, where a backdrop-filter makes WebRender allocate a render
243
253
  target spanning the whole scroll height — multi-GB on a long transcript. */
@@ -257,7 +267,7 @@ onBeforeUnmount(() => {
257
267
  .fade-slide-enter-from,
258
268
  .fade-slide-leave-to {
259
269
  opacity: 0;
260
- translate: -50% 8px;
270
+ translate: 0 8px;
261
271
  }
262
272
 
263
273
  @media (prefers-reduced-motion: reduce) {
@@ -79,6 +79,13 @@ export function useFollowPlayback(
79
79
  },
80
80
  )
81
81
 
82
+ // Re-enable follow on any seek (widget scrub or click-to-read): the user
83
+ // just told the player where to be, which is exactly what follow means to
84
+ // track — symmetric with the isPlaying re-enable above.
85
+ const stopSeekListener = core.on("audio:seek", () => {
86
+ isFollowing.value = true
87
+ })
88
+
82
89
  function onManualScroll() {
83
90
  isFollowing.value = false
84
91
  }
@@ -89,12 +96,14 @@ export function useFollowPlayback(
89
96
  }
90
97
  }
91
98
 
99
+ // wheel/touchstart/keydown are real scroll gestures — a bare pointerdown
100
+ // isn't (clicking a word, a button, anything in the panel triggers it
101
+ // without ever moving the scroll position), so it's deliberately excluded.
92
102
  function setupScrollListener(handler: (e: Event) => void) {
93
103
  const el = scrollContainer.value
94
104
  if (!el) return
95
105
  el.addEventListener("wheel", handler, { passive: true })
96
106
  el.addEventListener("touchstart", handler, { passive: true })
97
- el.addEventListener("pointerdown", handler, { passive: true })
98
107
  el.addEventListener("keydown", checkKeyDownIsScroll)
99
108
  }
100
109
 
@@ -103,7 +112,6 @@ export function useFollowPlayback(
103
112
  if (!el) return
104
113
  el.removeEventListener("wheel", handler)
105
114
  el.removeEventListener("touchstart", handler)
106
- el.removeEventListener("pointerdown", handler)
107
115
  el.removeEventListener("keydown", checkKeyDownIsScroll)
108
116
  }
109
117
 
@@ -113,6 +121,7 @@ export function useFollowPlayback(
113
121
 
114
122
  onBeforeUnmount(() => {
115
123
  downScrollListener(onManualScroll)
124
+ stopSeekListener()
116
125
  })
117
126
 
118
127
  function resumeFollow() {
package/src/core/types.ts CHANGED
@@ -30,6 +30,10 @@ export interface CoreEventMap {
30
30
  "speaker:add": { speaker: Speaker }
31
31
  "speaker:remove": { speakerId: string }
32
32
  "scroll:top": { translationId: string }
33
+ /** A seek was requested through core.audio.seekTo (widget scrub or
34
+ * click-to-read) — the explicit "take me to this instant" gesture that
35
+ * should resume follow-playback scrolling. */
36
+ "audio:seek": { time: number }
33
37
  "translation:sync": { translationId: string }
34
38
  "channel:sync": { channelId: string }
35
39
  "channel:reset": { channelId: string }