Skip to content

feat(packages): add chaptered time sliders - #2043

Merged
sampotts merged 1 commit into
mainfrom
codex/chapter-chunks
Aug 12, 2026
Merged

feat(packages): add chaptered time sliders#2043
sampotts merged 1 commit into
mainfrom
codex/chapter-chunks

Conversation

@sampotts

@sampotts sampotts commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Reworks the chaptered time slider based on the Notion review. It uses independently rendered HTML/CSS chapter segments rather than an SVG mask, while keeping the regular slider as a fallback until chapter cues are available.

Changes

  • Adds reusable chapter segment primitives for HTML and React, including custom rendering, normalized gaps, CSS variables, and interaction state.
  • Updates the Default and Minimal CSS and Tailwind skins with segmented track, buffer, fill, highlighting, and chapter preview details.
  • Adds an English chapter demo with a deliberate gap and coverage for the new core, HTML, and React behavior.

Testing

  • pnpm -F @videojs/html test src/define/tests/registration.test.ts
  • pnpm -F @videojs/react test src/ui/time-slider/tests/time-slider.test.tsx
  • pnpm -F @videojs/skins build
  • pnpm --dir apps/sandbox build

Note

Medium Risk
Touches the core time-slider interaction and Default/Minimal skins across HTML and React, so visual and seek UX regressions are possible. Changes are additive with a regular-track fallback until chapter cues load.

Overview
Adds chaptered time sliders that render independent HTML/CSS chapter segments instead of an SVG mask, with the regular track kept as a fallback until chapter cues are available.

Core introduces SliderSegmentsCore and TimeSliderChaptersCore to normalize cues (gaps, overlaps, clamping), expose per-chapter geometry/fill/buffer state, and drive chapter titles at the pointer or keyboard position.

HTML and React add TimeSlider.Chapters / media-time-slider-chapters plus chapter title parts, wired into Default and Minimal skins. Previews now show chapter titles alongside thumbnails/time, with highlight and gap styling.

Sandbox and docs demos include a chapters VTT source (with a deliberate gap) so the new segmented seek bar can be exercised end-to-end.

Reviewed by Cursor Bugbot for commit 0f0359e. Bugbot is set up for automated code reviews on this repo. Configure here.

@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 0f0359e
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a7bdee3216ba30008b5085e
😎 Deploy Preview https://deploy-preview-2043--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview Aug 12, 2026 2:48am

Request Review

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html

Path Base initial PR initial Initial diff Lazy diff
/video/minimal-skin 55.52 kB 57.27 kB +1.75 kB (+3.1%) 0 B
/video/minimal-skin.tailwind 56.12 kB 57.85 kB +1.73 kB (+3.1%) 0 B
/video/skin 55.93 kB 57.54 kB +1.61 kB (+2.9%) 0 B
/video/skin.tailwind 56.64 kB 58.19 kB +1.56 kB (+2.7%) 0 B
/audio/minimal-skin.css 4.11 kB 4.49 kB +396 B (+9.4%)
/audio/skin.css 4.05 kB 4.43 kB +384 B (+9.3%)
/audio/minimal-skin 44.29 kB 44.78 kB +509 B (+1.1%) 0 B
/audio/minimal-skin.tailwind 44.80 kB 45.23 kB +442 B (+1.0%) 0 B
/audio/skin 48.09 kB 48.53 kB +445 B (+0.9%) 0 B
/audio/skin.tailwind 48.56 kB 48.94 kB +397 B (+0.8%) 0 B
/live-audio/minimal-skin.css 4.11 kB 4.49 kB +396 B (+9.4%)
/live-audio/skin.css 4.05 kB 4.43 kB +384 B (+9.3%)
/live-audio/minimal-skin 36.63 kB 37.09 kB +469 B (+1.3%) 0 B
/live-audio/minimal-skin.tailwind 36.03 kB 36.49 kB +471 B (+1.3%) 0 B
/live-audio/skin 40.56 kB 40.96 kB +409 B (+1.0%) 0 B
/live-audio/skin.tailwind 40.06 kB 40.49 kB +439 B (+1.1%) 0 B
/ui/time-slider-chapter-title 8.26 kB new
/ui/time-slider-chapters 9.16 kB new
/video (default) 55.88 kB 57.57 kB +1.68 kB (+3.0%) 0 B
/video (default + hls) 194.04 kB 195.54 kB +1.50 kB (+0.8%) 0 B
/video (minimal) 55.57 kB 57.26 kB +1.69 kB (+3.0%) 0 B
/video (minimal + hls) 193.53 kB 195.25 kB +1.72 kB (+0.9%) 0 B
/audio (default) 48.12 kB 48.55 kB +441 B (+0.9%) 0 B
/audio (minimal) 44.32 kB 44.72 kB +406 B (+0.9%) 0 B
Small changes (35, ≤ 300 B)
Path Base initial PR initial Initial diff Lazy diff
/video/minimal-skin.css 5.92 kB 6.19 kB +280 B (+4.6%)
/video/skin.css 5.92 kB 6.12 kB +208 B (+3.4%)
/live-video/minimal-skin.css 5.92 kB 6.19 kB +280 B (+4.6%)
/live-video/skin.css 5.92 kB 6.12 kB +208 B (+3.4%)
/live-video/minimal-skin 51.83 kB 52.08 kB +253 B (+0.5%) 0 B
/live-video/minimal-skin.tailwind 52.42 kB 52.59 kB +174 B (+0.3%) 0 B
/live-video/skin 54.44 kB 54.59 kB +158 B (+0.3%) 0 B
/live-video/skin.tailwind 54.94 kB 55.12 kB +180 B (+0.3%) 0 B
/ui/airplay-button 10.79 kB 10.80 kB +5 B (+0.0%) 0 B
/ui/alert-dialog 4.14 kB 4.15 kB +14 B (+0.3%)
/ui/alert-dialog-description 1.61 kB 1.62 kB +18 B (+1.1%)
/ui/alert-dialog-title 1.61 kB 1.62 kB +15 B (+0.9%)
/ui/cast-button 10.77 kB 10.77 kB -2 B (-0.0%) 0 B
/ui/compounds 36.16 kB 36.18 kB +22 B (+0.1%) 0 B
/ui/controls 8.21 kB 8.21 kB +6 B (+0.1%) 0 B
/ui/error-dialog 11.20 kB 11.22 kB +19 B (+0.2%) 0 B
/ui/fullscreen-button 10.74 kB 10.74 kB +3 B (+0.0%) 0 B
/ui/menu 20.64 kB 20.66 kB +29 B (+0.1%) 0 B
/ui/mute-button 10.75 kB 10.77 kB +18 B (+0.2%) 0 B
/ui/pip-button 10.73 kB 10.75 kB +12 B (+0.1%) 0 B
/ui/play-button 10.73 kB 10.74 kB +13 B (+0.1%) 0 B
/ui/playback-rate-button 10.93 kB 10.93 kB -4 B (-0.0%) 0 B
/ui/playback-rate-radio-group 10.34 kB 10.35 kB +10 B (+0.1%) 0 B
/ui/popover 7.78 kB 7.77 kB -3 B (-0.0%)
/ui/quality-radio-group 11.12 kB 11.12 kB +8 B (+0.1%) 0 B
/ui/seek-button 10.79 kB 10.80 kB +11 B (+0.1%) 0 B
/ui/seek-indicator 12.51 kB 12.51 kB +5 B (+0.0%)
/ui/slider 11.61 kB 11.62 kB +15 B (+0.1%) 0 B
/ui/status-announcer 10.14 kB 10.14 kB -1 B (-0.0%) 0 B
/ui/status-indicator 13.03 kB 13.02 kB -5 B (-0.0%) 0 B
/ui/thumbnail 9.17 kB 9.17 kB +2 B (+0.0%)
/ui/time-slider 14.51 kB 14.55 kB +38 B (+0.3%) 0 B
/ui/tooltip 9.06 kB 9.06 kB -2 B (-0.0%) 0 B
/ui/volume-indicator 13.03 kB 13.04 kB +2 B (+0.0%) 0 B
/ui/volume-slider 12.22 kB 12.23 kB +13 B (+0.1%) 0 B
Presets (7)
Entry Initial Lazy
/video (default) 57.57 kB 56.93 kB
/video (default + hls) 195.54 kB 56.93 kB
/video (minimal) 57.26 kB 56.93 kB
/video (minimal + hls) 195.25 kB 56.93 kB
/audio (default) 48.55 kB 56.93 kB
/audio (minimal) 44.72 kB 56.93 kB
/background 5.59 kB 56.93 kB
Media (13)
Entry Initial Lazy
/media/background-video 1.14 kB
/media/container 2.58 kB 56.93 kB
/media/dash-video 211.36 kB
/media/google-cast 6.44 kB
/media/hlsjs-video 140.20 kB
/media/mux-audio 140.92 kB
/media/mux-data 26.69 kB
/media/mux-video 141.13 kB
/media/native-hls-video 7.51 kB
/media/simple-hls-audio-only 21.59 kB
/media/simple-hls-video 27.08 kB
/media/vimeo-video 12.58 kB
/media/youtube-video 6.67 kB
Players (5)
Entry Initial Lazy
/video/player 10.02 kB 56.93 kB
/audio/player 6.99 kB 56.93 kB
/background/player 5.32 kB 56.93 kB
/live-video/player 9.42 kB 56.93 kB
/live-audio/player 7.01 kB 56.93 kB
Skins (30)
Entry Type Initial Lazy
/video/minimal-skin.css css 6.19 kB
/video/skin.css css 6.12 kB
/video/minimal-skin js 57.27 kB 56.93 kB
/video/minimal-skin.tailwind js 57.85 kB 56.93 kB
/video/skin js 57.54 kB 56.93 kB
/video/skin.tailwind js 58.19 kB 56.93 kB
/audio/minimal-skin.css css 4.49 kB
/audio/skin.css css 4.43 kB
/audio/minimal-skin js 44.78 kB 56.93 kB
/audio/minimal-skin.tailwind js 45.23 kB 56.93 kB
/audio/skin js 48.53 kB 56.93 kB
/audio/skin.tailwind js 48.94 kB 56.93 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 6.19 kB
/live-video/skin.css css 6.12 kB
/live-video/minimal-skin js 52.08 kB 56.93 kB
/live-video/minimal-skin.tailwind js 52.59 kB 56.93 kB
/live-video/skin js 54.59 kB 56.93 kB
/live-video/skin.tailwind js 55.12 kB 56.93 kB
/live-audio/minimal-skin.css css 4.49 kB
/live-audio/skin.css css 4.43 kB
/live-audio/minimal-skin js 37.09 kB 56.93 kB
/live-audio/minimal-skin.tailwind js 36.49 kB 56.93 kB
/live-audio/skin js 40.96 kB 56.93 kB
/live-audio/skin.tailwind js 40.49 kB 56.93 kB
/global.css css 183 B
/shared.css css 104 B
/tailwind.css css 161 B
/skin-element js 1.45 kB
UI Components (41)
Entry Initial Lazy
/ui/airplay-button 10.80 kB 56.93 kB
/ui/alert-dialog 4.15 kB
/ui/alert-dialog-close 1.94 kB
/ui/alert-dialog-description 1.62 kB
/ui/alert-dialog-title 1.62 kB
/ui/audio-track-radio-group 10.56 kB 56.93 kB
/ui/buffering-indicator 8.04 kB
/ui/captions-button 11.06 kB 56.93 kB
/ui/captions-radio-group 10.62 kB 56.93 kB
/ui/cast-button 10.77 kB 56.93 kB
/ui/compounds 36.18 kB 56.93 kB
/ui/controls 8.21 kB 56.93 kB
/ui/error-dialog 11.22 kB 56.93 kB
/ui/fullscreen-button 10.74 kB 56.93 kB
/ui/hotkey 9.32 kB
/ui/menu 20.66 kB 56.93 kB
/ui/mute-button 10.77 kB 56.93 kB
/ui/pip-button 10.75 kB 56.93 kB
/ui/play-button 10.74 kB 56.93 kB
/ui/playback-rate-button 10.93 kB 56.93 kB
/ui/playback-rate-radio-group 10.35 kB 56.93 kB
/ui/popover 7.77 kB
/ui/poster 8.40 kB 56.93 kB
/ui/quality-radio-group 11.12 kB 56.93 kB
/ui/seek-button 10.80 kB 56.93 kB
/ui/seek-indicator 12.51 kB
/ui/seek-indicator-value 1.02 kB
/ui/slider 11.62 kB 56.93 kB
/ui/status-announcer 10.14 kB 56.93 kB
/ui/status-indicator 13.02 kB 56.93 kB
/ui/status-indicator-value 1.02 kB
/ui/thumbnail 9.17 kB
/ui/time 10.53 kB 56.93 kB
/ui/time-slider 14.55 kB 56.93 kB
/ui/time-slider-chapter-title 8.26 kB
/ui/time-slider-chapters 9.16 kB
/ui/tooltip 9.06 kB 56.93 kB
/ui/volume-indicator 13.04 kB 56.93 kB
/ui/volume-indicator-fill 1.03 kB
/ui/volume-indicator-value 1.02 kB
/ui/volume-slider 12.23 kB 56.93 kB

⚛️ @videojs/react

Path Base initial PR initial Initial diff Lazy diff
/video/minimal-skin 43.46 kB 44.93 kB +1.47 kB (+3.4%) 0 B
/video/minimal-skin.tailwind 49.71 kB 51.45 kB +1.73 kB (+3.5%) 0 B
/video/skin 43.38 kB 44.93 kB +1.55 kB (+3.6%) 0 B
/video/skin.tailwind 49.71 kB 51.38 kB +1.66 kB (+3.3%) 0 B
/audio/minimal-skin.css 3.97 kB 4.35 kB +393 B (+9.7%)
/audio/skin.css 3.90 kB 4.28 kB +386 B (+9.7%)
/audio/minimal-skin.tailwind 38.35 kB 38.75 kB +408 B (+1.0%) 0 B
/audio/skin.tailwind 40.15 kB 40.49 kB +352 B (+0.9%) 0 B
/live-audio/minimal-skin.css 3.97 kB 4.35 kB +393 B (+9.7%)
/live-audio/skin.css 3.90 kB 4.28 kB +386 B (+9.7%)
/live-audio/minimal-skin.tailwind 29.71 kB 30.16 kB +466 B (+1.5%) 0 B
/live-audio/skin.tailwind 29.86 kB 30.22 kB +366 B (+1.2%) 0 B
/ui/time-slider 12.08 kB 13.60 kB +1.52 kB (+12.6%) 0 B
/video (default) 43.48 kB 44.98 kB +1.50 kB (+3.5%) 0 B
/video (default + hls) 180.41 kB 181.71 kB +1.30 kB (+0.7%) 0 B
/video (minimal) 43.58 kB 45.06 kB +1.48 kB (+3.4%) 0 B
/video (minimal + hls) 180.64 kB 181.83 kB +1.19 kB (+0.7%) 0 B
Small changes (36, ≤ 300 B)
Path Base initial PR initial Initial diff Lazy diff
/video/minimal-skin.css 5.81 kB 6.08 kB +272 B (+4.6%)
/video/skin.css 5.81 kB 6.02 kB +212 B (+3.6%)
/audio/minimal-skin 35.82 kB 35.84 kB +14 B (+0.0%) 0 B
/audio/skin 35.79 kB 35.78 kB -1 B (-0.0%) 0 B
/live-video/minimal-skin.css 5.81 kB 6.08 kB +272 B (+4.6%)
/live-video/skin.css 5.81 kB 6.02 kB +212 B (+3.6%)
/live-video/minimal-skin 38.52 kB 38.52 kB -1 B (-0.0%) 0 B
/live-video/minimal-skin.tailwind 44.70 kB 44.90 kB +210 B (+0.5%) 0 B
/live-video/skin 38.53 kB 38.51 kB -12 B (-0.0%) 0 B
/live-video/skin.tailwind 44.82 kB 45.01 kB +193 B (+0.4%) 0 B
/live-audio/minimal-skin 26.40 kB 26.41 kB +10 B (+0.0%) 0 B
/live-audio/skin 26.45 kB 26.46 kB +15 B (+0.1%) 0 B
/ui/alert-dialog 3.25 kB 3.26 kB +5 B (+0.2%)
/ui/captions-button 10.01 kB 10.01 kB +1 B (+0.0%) 0 B
/ui/cast-button 10.02 kB 10.01 kB -7 B (-0.1%) 0 B
/ui/controls 6.98 kB 6.97 kB -2 B (-0.0%)
/ui/error-dialog 9.98 kB 9.99 kB +4 B (+0.0%) 0 B
/ui/fullscreen-button 9.98 kB 9.98 kB +2 B (+0.0%) 0 B
/ui/menu 20.62 kB 20.63 kB +9 B (+0.0%) 0 B
/ui/mute-button 9.98 kB 9.97 kB -3 B (-0.0%) 0 B
/ui/playback-rate 6.79 kB 6.79 kB +1 B (+0.0%)
/ui/playback-rate-button 9.96 kB 9.96 kB -2 B (-0.0%) 0 B
/ui/popover 7.47 kB 7.47 kB -1 B (-0.0%)
/ui/poster 6.86 kB 6.87 kB +8 B (+0.1%)
/ui/quality 8.01 kB 8.01 kB -2 B (-0.0%) 0 B
/ui/seek-button 10.00 kB 9.99 kB -1 B (-0.0%) 0 B
/ui/slider 11.96 kB 11.99 kB +30 B (+0.2%) 0 B
/ui/status-announcer 9.68 kB 9.69 kB +7 B (+0.1%) 0 B
/ui/status-indicator 12.27 kB 12.26 kB -3 B (-0.0%) 0 B
/ui/thumbnail 8.07 kB 8.10 kB +29 B (+0.4%)
/ui/time 9.47 kB 9.47 kB +2 B (+0.0%) 0 B
/ui/tooltip 8.21 kB 8.22 kB +4 B (+0.0%)
/ui/volume-indicator 12.37 kB 12.37 kB +2 B (+0.0%) 0 B
/ui/volume-slider 11.39 kB 11.41 kB +15 B (+0.1%) 0 B
/audio (default) 35.90 kB 35.89 kB -10 B (-0.0%) 0 B
/audio (minimal) 35.93 kB 35.94 kB +14 B (+0.0%) 0 B
Presets (7)
Entry Initial Lazy
/video (default) 44.98 kB 56.93 kB
/video (default + hls) 181.71 kB 56.93 kB
/video (minimal) 45.06 kB 56.93 kB
/video (minimal + hls) 181.83 kB 56.93 kB
/audio (default) 35.89 kB 56.93 kB
/audio (minimal) 35.94 kB 56.93 kB
/background 581 B
Media (12)
Entry Initial
/media/background-video 394 B
/media/dash-video 209.68 kB
/media/google-cast 5.34 kB
/media/hlsjs-video 138.60 kB
/media/mux-audio 139.38 kB
/media/mux-data 25.47 kB
/media/mux-video 139.43 kB
/media/native-hls-video 5.75 kB
/media/simple-hls-audio-only 19.89 kB
/media/simple-hls-video 25.41 kB
/media/vimeo-video 10.75 kB
/media/youtube-video 4.78 kB
Skins (27)
Entry Type Initial Lazy
/tailwind.css css 161 B
/video/minimal-skin.css css 6.08 kB
/video/skin.css css 6.02 kB
/video/minimal-skin js 44.93 kB 56.93 kB
/video/minimal-skin.tailwind js 51.45 kB 56.93 kB
/video/skin js 44.93 kB 56.93 kB
/video/skin.tailwind js 51.38 kB 56.93 kB
/audio/minimal-skin.css css 4.35 kB
/audio/skin.css css 4.28 kB
/audio/minimal-skin js 35.84 kB 56.93 kB
/audio/minimal-skin.tailwind js 38.75 kB 56.93 kB
/audio/skin js 35.78 kB 56.93 kB
/audio/skin.tailwind js 40.49 kB 56.93 kB
/background/skin.css css 90 B
/background/skin js 273 B
/live-video/minimal-skin.css css 6.08 kB
/live-video/skin.css css 6.02 kB
/live-video/minimal-skin js 38.52 kB 56.93 kB
/live-video/minimal-skin.tailwind js 44.90 kB 56.93 kB
/live-video/skin js 38.51 kB 56.93 kB
/live-video/skin.tailwind js 45.01 kB 56.93 kB
/live-audio/minimal-skin.css css 4.35 kB
/live-audio/skin.css css 4.28 kB
/live-audio/minimal-skin js 26.41 kB 56.93 kB
/live-audio/minimal-skin.tailwind js 30.16 kB 56.93 kB
/live-audio/skin js 26.46 kB 56.93 kB
/live-audio/skin.tailwind js 30.22 kB 56.93 kB
UI Components (33)
Entry Initial Lazy
/ui/airplay-button 10.04 kB 56.93 kB
/ui/alert-dialog 3.26 kB
/ui/audio-track 7.49 kB 56.93 kB
/ui/buffering-indicator 7.26 kB
/ui/captions-button 10.01 kB 56.93 kB
/ui/captions-radio-group 7.62 kB 56.93 kB
/ui/cast-button 10.01 kB 56.93 kB
/ui/controls 6.97 kB
/ui/error-dialog 9.99 kB 56.93 kB
/ui/fullscreen-button 9.98 kB 56.93 kB
/ui/gesture 7.37 kB
/ui/hotkey 7.81 kB
/ui/live-button 8.42 kB 56.93 kB
/ui/menu 20.63 kB 56.93 kB
/ui/mute-button 9.97 kB 56.93 kB
/ui/pip-button 9.98 kB 56.93 kB
/ui/play-button 9.95 kB 56.93 kB
/ui/playback-rate 6.79 kB
/ui/playback-rate-button 9.96 kB 56.93 kB
/ui/popover 7.47 kB
/ui/poster 6.87 kB
/ui/quality 8.01 kB 56.93 kB
/ui/seek-button 9.99 kB 56.93 kB
/ui/seek-indicator 11.80 kB
/ui/slider 11.99 kB 56.93 kB
/ui/status-announcer 9.69 kB 56.93 kB
/ui/status-indicator 12.26 kB 56.93 kB
/ui/thumbnail 8.10 kB
/ui/time 9.47 kB 56.93 kB
/ui/time-slider 13.60 kB 56.93 kB
/ui/tooltip 8.22 kB
/ui/volume-indicator 12.37 kB 56.93 kB
/ui/volume-slider 11.41 kB 56.93 kB

🧩 @videojs/core

Path Base initial PR initial Initial diff Lazy diff
. 11.77 kB 12.68 kB +929 B (+7.7%)
Small changes (1, ≤ 300 B)
Path Base initial PR initial Initial diff Lazy diff
/dom 20.00 kB 20.02 kB +25 B (+0.1%)
Entries (76)
Entry Initial Lazy
. 12.68 kB
/dom 20.02 kB
/components 900 B
/i18n 3.11 kB 56.93 kB
/i18n/locales/all 35.05 kB
/i18n/locales/ar 1.22 kB
/i18n/locales/az 1.09 kB
/i18n/locales/bg 1.29 kB
/i18n/locales/bn 1.28 kB
/i18n/locales/bs 1.00 kB
/i18n/locales/ca 1.07 kB
/i18n/locales/cs 1.07 kB
/i18n/locales/cy 1.02 kB
/i18n/locales/da 1006 B
/i18n/locales/de 1.09 kB
/i18n/locales/el 1.49 kB
/i18n/locales/en 785 B
/i18n/locales/es 1018 B
/i18n/locales/et 1.05 kB
/i18n/locales/eu 1.03 kB
/i18n/locales/fa 1.22 kB
/i18n/locales/fi 1.04 kB
/i18n/locales/fr 1.07 kB
/i18n/locales/gd 1.10 kB
/i18n/locales/gl 1022 B
/i18n/locales/he 1.14 kB
/i18n/locales/hi 1.30 kB
/i18n/locales/hr 1.02 kB
/i18n/locales/hu 1.10 kB
/i18n/locales/id 919 B
/i18n/locales/it 1.01 kB
/i18n/locales/ja 1.18 kB
/i18n/locales/ko 1.14 kB
/i18n/locales/lt 1.02 kB
/i18n/locales/lv 1.09 kB
/i18n/locales/mr 1.30 kB
/i18n/locales/nb 987 B
/i18n/locales/ne 1.29 kB
/i18n/locales/nl 1.00 kB
/i18n/locales/nn 987 B
/i18n/locales/oc 1.08 kB
/i18n/locales/pl 1.14 kB
/i18n/locales/pt 1.03 kB
/i18n/locales/pt-BR 1.03 kB
/i18n/locales/pt-PT 1.01 kB
/i18n/locales/ro 1.07 kB
/i18n/locales/ru 1.40 kB
/i18n/locales/sk 1.10 kB
/i18n/locales/sl 1.04 kB
/i18n/locales/sr 1.07 kB
/i18n/locales/sv 1.01 kB
/i18n/locales/te 1.32 kB
/i18n/locales/th 1.29 kB
/i18n/locales/tr 1.08 kB
/i18n/locales/uk 1.41 kB
/i18n/locales/vi 1.11 kB
/i18n/locales/zh 1.02 kB
/i18n/locales/zh-CN 1.02 kB
/i18n/locales/zh-TW 1.02 kB
/i18n/text/airplay 101 B
/i18n/text/buttons 137 B
/i18n/text/captions 93 B
/i18n/text/cast 114 B
/i18n/text/common 90 B
/i18n/text/container 75 B
/i18n/text/errors 285 B
/i18n/text/fullscreen 98 B
/i18n/text/live 126 B
/i18n/text/menu 249 B
/i18n/text/pip 101 B
/i18n/text/playback 80 B
/i18n/text/seek 105 B
/i18n/text/slider 65 B
/i18n/text/status 222 B
/i18n/text/time 280 B
/i18n/text/volume 133 B
🏷️ @videojs/element — no changes
Entries (2)
Entry Initial
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Initial
. 1.71 kB
/html 703 B
/react 366 B
🔧 @videojs/utils — 2 small size changes
Path Base initial PR initial Initial diff Lazy diff
/array 104 B 271 B +167 B (+160.6%)
/number 158 B 199 B +41 B (+25.9%)
Entries (13)
Entry Initial
/array 271 B
/dom 3.29 kB
/events 320 B
/function 369 B
/i18n 44 B
/jwt 176 B
/object 508 B
/predicate 297 B
/percent 281 B
/string 239 B
/style 188 B
/time 1.01 kB
/number 199 B
📦 @videojs/jsx — no changes
Entries (3)
Entry Initial
. 393 B
/jsx-runtime 371 B
/jsx-dev-runtime 375 B
📦 @videojs/media — no changes
Entries (15)
Entry Initial
. 1.04 kB
/dom 121 B
/dom/audio-host 1.10 kB
/dom/custom-media-element 2.09 kB
/dom/dash 209.38 kB
/dom/google-cast 4.06 kB
/dom/hls-js 138.13 kB
/dom/media-host 1.20 kB
/dom/media-played-ranges 576 B
/dom/mux 161.61 kB
/dom/native-hls 5.26 kB
/dom/video-host 1.39 kB
/dom/vimeo 10.19 kB
/dom/youtube 4.17 kB
/media-tracks 1.98 kB
📦 @videojs/spf — no changes
Entries (7)
Entry Initial
. 4.53 kB
/dom 6.57 kB
/hls 20.86 kB
/media-tracks 504 B
/background-video 14.77 kB
/simple-hls 24.92 kB
/simple-hls-audio-only 19.39 kB

ℹ️ How to interpret

Each entry is independently bundled, minified, and brotli-compressed. Initial size includes its static import graph; lazy dynamic chunks are reported separately.

Entries are not additive because their dependency graphs overlap. Preset rows represent realistic combined bundles.

Changes of 300 B or less across initial, lazy, and total size are collapsed, not discarded.

Run pnpm size locally to check current initial sizes.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b105422. Configure here.

Comment thread packages/react/src/ui/time-slider/time-slider-chapters.tsx Outdated
Comment thread packages/react/src/ui/time-slider/time-slider-chapter-title.tsx Outdated
</TimeSlider.Track>
<TimeSlider.Chapters
className={slider.chapters}
renderChapter={(props) => (

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept it renderChapter as it's not really rendering the element for TimeSlider.Chapters itself but I can change that if you preferred.

@mihar-22 mihar-22 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this is looking really good 😎 Great work! Thank you for taking in all the feedback and finding a workaround on the single track issue.

I did a AI review locally and it came up with these findings - might be worth having your agent review:

Core Assessment

Core ownership is directionally correct:

  • Text-track discovery remains in the existing DOM store feature.
  • Cue normalization and generic segment geometry are framework-neutral.
  • React and HTML use the same data attributes and CSS-variable names.
  • Registration uses narrow safeDefine entry points; classes do not self-register.
  • The segment collection is decorative while the existing slider thumb retains the interactive ARIA contract.

Core is not yet doing enough of the feature derivation. HTML and React still independently own chapter lookup, buffer localization, fallback rules, and normalization scheduling. In HTML, both the segment collection and title renormalize on slider updates, including pointer movement.

I would make TimeSliderChaptersCore own:

  • normalized ranges and hasChapters;
  • raw pointer-to-chapter lookup;
  • local fill/buffer calculation;
  • the final per-chapter state.

The platform layers could then limit themselves to store subscriptions and rendering. That would remove the React root-state workaround and make HTML/React parity easier to enforce.

The public documentation also needs a chaptered anatomy/example. The current [TimeSlider reference](

## Anatomy
<FrameworkCase frameworks={["react"]}>
```tsx
<TimeSlider.Root />
```
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
```html
<media-time-slider></media-time-slider>
```
</FrameworkCase>
## Behavior
Displays and controls the current playback position. Dragging the slider seeks the media. The fill level reflects `currentTime / duration` as a percentage, and the buffer level shows how much media has been buffered.
Value changes during drag are throttled via the `changeThrottle` prop (default 100ms) using a leading+trailing throttle to keep the UI responsive without overwhelming the media element.
## Styling
Use [CSS custom properties](#root-css-custom-properties) to style the fill, pointer, and buffer levels:
) does not describe Chapters, ChapterTitle, renderChapter, the HTML template/fallback contract, synthetic gaps, CSS variables, or data attributes.

Local AI Review
  1. Medium — The final chapter disappears at the slider’s exact right edge

    Both adapters publish pointerValue using valueFromPercent(), which clamps and then rounds to the slider step. With the demo’s 487.626s duration and default one-second step, 100% becomes 488, outside the normalized chapter domain. Consequently, neither findActive() nor SliderSegmentsCore considers the last chapter active.

    I reproduced this in the deployed HTML and React previews: hovering at 80% showed “Track Performance”; pressing End or pointing at the right edge produced an empty chapter title.

    Relevant code: [slider-core.ts](

    valueFromPercent(percent: number): number {
    const { min, max, step } = this.#props;
    const raw = min + (percent / 100) * (max - min);
    return roundToStep(clamp(raw, min, max), step, min);
    }
    /** Convert percent to a clamped value without applying step rounding. */
    rawValueFromPercent(percent: number): number {
    const { min, max } = this.#props;
    return clamp(min + (percent / 100) * (max - min), min, max);
    ), [React context](
    return (
    <SliderProvider
    value={{
    state,
    pointerValue: core.valueFromPercent(state.pointerPercent),
    thumbRef,
    ), [HTML context](
    // Provide context to child elements with base slider data attrs.
    this.#provider.setValue({
    state,
    stateAttrMap: TimeSliderDataAttrs,
    pointerValue: this.#core.valueFromPercent(state.pointerPercent),
    thumbAttrs: {
    ).

    Smallest fix: clamp again after rounding. Better for chapter hit-testing: expose/use rawValueFromPercent() so hover containment is continuous rather than quantized to the seek step. Add a test where duration is not divisible by step.

  2. Medium — An invalid HTML template silently removes the entire track

    The HTML component hides the fallback as soon as chapter cues exist, then silently skips a chapter if the template does not contain exactly one HTMLElement. A template with two roots, an SVG root, or only text therefore results in no rendered chapters and no fallback.

    The one-HTMLElement constraint is also absent from the public JSDoc. Relevant ordering: [time-slider-chapters-element.ts](

    const chapters = normalizeChapterCues(this.#textTrack.value?.chaptersCues ?? [], 0, duration);
    const hasChapters = chapters.some((chapter) => chapter.cue !== null);
    this.#showFallback(!hasChapters);
    if (!hasChapters) return;
    const geometry = this.#core.getGeometry({
    ranges: chapters.map(({ key, start, end, cue }) => ({ key, start, end, highlight: cue !== null })),
    min: 0,
    max: duration,
    orientation: slider.state.orientation,
    });
    const buffered = this.#buffer.value?.buffered ?? [];
    const bufferedEnd = buffered.length ? buffered[buffered.length - 1]![1] : 0;
    const next = new Map<string, HTMLElement>();
    for (const segment of geometry) {
    const state = this.#core.getState(segment, slider.state, slider.pointerValue);
    let root = this.#rendered.get(state.key);
    if (!root) {
    const fragment = template.content.cloneNode(true) as DocumentFragment;
    const first = fragment.firstElementChild;
    if (!(first instanceof HTMLElement) || first.nextElementSibling) continue;
    ).

    Smallest fix: validate the template once before hiding the fallback, retaining the fallback and emitting a development warning when invalid. Alternatively, support a fragment/multiple roots. Tests should cover missing, empty, multiple-root, and non-HTML templates.

  3. Medium — React’s collection-level state is arbitrarily the first chapter

    TimeSliderChaptersProps exposes className, style, and render as functions of TimeSliderChapterState, but SliderSegments supplies the state of geometry[0] to those collection-level callbacks.

    That means:

    • The collection’s state always describes the first chapter.
    • Moving playback into another chapter does not make the collection callback describe that chapter.
    • The fallback path fabricates a pseudo chapter state.
    • There is no documented way for consumers to know why this particular chapter was selected.

    Relevant code: [public props](

    export interface TimeSliderChapterState extends SliderSegmentState {
    /** Authored chapter cue, or `null` for an uncovered interval. */
    cue: MediaTextCue | null;
    /** Buffer from 0–100 relative to this chapter. */
    bufferPercent: number;
    }
    export interface TimeSliderChaptersProps extends Omit<UIComponentProps<'div', TimeSliderChapterState>, 'children'> {
    /** Slider track rendered until authored chapters are available. */
    children?: ReactNode;
    /** Render one consumer-owned subtree for every normalized chapter. */
    renderChapter: (props: Omit<HTMLProps<HTMLElement>, 'ref'>, state: TimeSliderChapterState) => ReactElement;
    ), [first-segment selection](
    const state = geometry.length > 0 ? core.getState(geometry[0]!, slider.state, slider.pointerValue) : null;
    if (!state) return null;
    return renderElement(
    'div',
    { render, className, style },
    {
    state,
    ref,
    props: [{ 'aria-hidden': 'true' }, sliderAttrs, elementProps, { children: children ?? segments }],
    ).

    Smallest fix: give the root a collection-level state such as { hasChapters, chapters }, or use the parent slider state. Reserve TimeSliderChapterState exclusively for renderChapter.

  4. Low — Chapter information remains pointer-only and is not coherently exposed to keyboard/screen-reader users

    The preview is revealed only by data-pointing; keyboard focus and arrow/Home/End interaction do not reveal the chapter title. The slider’s aria-valuetext continues to contain only time. Meanwhile, because the opacity-zero title is not hidden from accessibility APIs, the browser accessibility tree includes the preview title as detached child text—potentially stale at the default pointer position.

    Relevant CSS: [default slider.css](

    & .media-slider__thumbnail,
    & .media-slider__value {
    position: absolute;
    left: 50%;
    max-width: var(--max-width);
    opacity: 0;
    filter: blur(8px);
    transform-origin: bottom;
    scale: 0.8;
    translate: -50% calc(var(--spacing) * 2);
    transition-timing-function: ease-out;
    transition-duration: 150ms;
    }
    & .media-slider__thumbnail {
    --thumbnail-max-width: var(--max-width);
    bottom: calc(100% + (var(--spacing) * 9));
    }
    & .media-slider__value {
    bottom: calc(100% + (var(--spacing) * 10.5));
    display: flex;
    flex-direction: column;
    align-items: center;
    text-shadow: 0 1px 0 var(--media-current-shadow-color);
    }
    & .media-slider__chapter-title {
    min-width: 0;
    max-width: var(--max-width);
    padding-inline: calc(var(--spacing) * 3);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    &:empty {
    display: none;
    }
    }
    &::before {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 1;
    width: calc(var(--spacing) * 1);
    height: calc(var(--spacing) * 1);
    pointer-events: none;
    content: "";
    background-color: currentColor;
    border-radius: 100%;
    box-shadow:
    0 0 0 1px var(--media-current-shadow-color, oklch(0 0 0 / 0.15)),
    0 1px 2px 0 oklch(0 0 0 / 0.35);
    opacity: 0;
    scale: 0.5;
    translate: -50% -50%;
    transition-timing-function: ease-out;
    transition-duration: 200ms;
    transition-property: opacity, scale;
    }
    &[data-pointing] .media-slider__value,
    &[data-pointing] .media-slider__thumbnail {
    opacity: 1;
    filter: blur(0);
    scale: 1;
    }
    ). The title is given aria-live="off" but no explicit association with the slider: [React title](
    return renderElement(
    'span',
    { render, className, style },
    {
    state,
    ref,
    props: [{ 'aria-live': 'off', children: state.text }, elementProps],
    }
    ).

    Consider showing the preview on keyboard focus, resolving against current time when no pointer interaction exists, and deliberately including/associating the chapter title with the slider’s accessible value. Otherwise mark the visual preview hidden from accessibility APIs.

@mihar-22 mihar-22 Aug 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should move time slider chapters in a separate directory time-slider-chapters to follow conventions, separate component

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

including the core.ts file too

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure? it seems like media-time-slider is still the parent. or do you mean a new folder in the time-slider folder?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was thinking originally top-level, but maybe you're right this is directly tied to time-slider so a subdir would make more sense 👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to differentiate between standard parts (track, fill, buffer, thumb) from opt-in stuff like chapters which are heavy

Comment on lines +9 to +16
/** CSS geometry and progress local to each chapter. */
export const TimeSliderChapterCSSVars = {
start: '--media-slider-chapter-start',
end: '--media-slider-chapter-end',
width: '--media-slider-chapter-width',
fill: '--media-slider-chapter-fill',
buffer: '--media-slider-chapter-buffer',
} as const;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate file to follow conventions time-slider-chapters-css-vars.ts. I think we can do a follow up to simplify these names to not include the component name, so just data-attrs.ts, core.ts, and css-vars.ts

Comment on lines +4 to +7
export interface TimeSliderChapterRange extends SliderSegmentRange {
/** Authored chapter cue, or `null` for an uncovered interval. */
cue: MediaTextCue | null;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe move to a sibling types.ts - not important

Comment thread packages/html/src/define/ui/compounds.ts Outdated
Comment thread packages/html/src/define/video/minimal-skin.tailwind.ts Outdated
Comment on lines +71 to +75
className={typeof className === 'function' ? (state) => className(getChapterState(state)) : className}
style={typeof style === 'function' ? (state) => style(getChapterState(state)) : style}
render={
typeof render === 'function' ? (renderProps, state) => render(renderProps, getChapterState(state)) : render
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have isFunction utility we can use here

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont know for sure but isn't this normally abstracted into core

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved it to core 👍🏼

Comment thread packages/utils/src/array/find-active.ts Outdated
@@ -0,0 +1,29 @@
/** Finds the active item in a collection ordered by its start value. */
export function findActive<Item>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name wasn't feeling clear, spiked with AI quickly to refactor into separate utils:

function findLastIndexAtOrBefore<Item>(
  items: readonly Item[],
  value: number,
  getValue: (item: Item) => number
): number {
  let low = 0;
  let high = items.length - 1;
  let index = -1;

  while (low <= high) {
    const mid = (low + high) >>> 1;

    if (getValue(items[mid]!) <= value) {
      index = mid;
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }

  return index;
}

export function findLastAtOrBefore<Item>(
  items: readonly Item[],
  value: number,
  getValue: (item: Item) => number
): Item | undefined {
  const index = findLastIndexAtOrBefore(items, value, getValue);

  return index < 0 ? undefined : items[index];
}

export function findRangeAt<Range>(
  ranges: readonly Range[],
  value: number,
  getStart: (range: Range) => number,
  getEnd: (range: Range) => number
): Range | undefined {
  const index = findLastIndexAtOrBefore(ranges, value, getStart);

  if (index < 0) return undefined;

  const range = ranges[index]!;
  const end = getEnd(range);
  const isLastRange = index === ranges.length - 1;

  if (value < end || (isLastRange && value === end)) {
    return range;
  }

  return undefined;
}

@sampotts
sampotts force-pushed the codex/chapter-chunks branch from 90fa7d8 to 0f0359e Compare August 12, 2026 02:48
@sampotts
sampotts merged commit 6490051 into main Aug 12, 2026
31 checks passed
@sampotts
sampotts deleted the codex/chapter-chunks branch August 12, 2026 03:06
@luwes luwes mentioned this pull request Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants