cursedbelt 3.0.2 → 4.0.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 (250) hide show
  1. package/dist/react/components/ListToolbar.d.ts +94 -0
  2. package/dist/react/components/ListToolbar.d.ts.map +1 -0
  3. package/dist/react/components/ListToolbar.js +66 -0
  4. package/dist/react/components/ListToolbar.js.map +1 -0
  5. package/dist/react/file-tree/FileTree.d.ts +282 -3
  6. package/dist/react/file-tree/FileTree.d.ts.map +1 -1
  7. package/dist/react/file-tree/FileTree.js +799 -131
  8. package/dist/react/file-tree/FileTree.js.map +1 -1
  9. package/dist/react/file-tree/fileTree.css +151 -0
  10. package/dist/react/file-tree/fileTreeSelection.d.ts +187 -0
  11. package/dist/react/file-tree/fileTreeSelection.d.ts.map +1 -0
  12. package/dist/react/file-tree/fileTreeSelection.js +264 -0
  13. package/dist/react/file-tree/fileTreeSelection.js.map +1 -0
  14. package/dist/react/file-tree/index.d.ts +1 -0
  15. package/dist/react/file-tree/index.d.ts.map +1 -1
  16. package/dist/react/file-tree/index.js +8 -0
  17. package/dist/react/file-tree/index.js.map +1 -1
  18. package/dist/react/index.d.ts +1 -0
  19. package/dist/react/index.d.ts.map +1 -1
  20. package/dist/react/index.js +1 -0
  21. package/dist/react/index.js.map +1 -1
  22. package/dist/react/lib/form.d.ts +3 -0
  23. package/dist/react/lib/form.d.ts.map +1 -1
  24. package/dist/react/lib/form.js +43 -5
  25. package/dist/react/lib/form.js.map +1 -1
  26. package/dist/react/media/DenseGalleryShell.d.ts +19 -3
  27. package/dist/react/media/DenseGalleryShell.d.ts.map +1 -1
  28. package/dist/react/media/DenseGalleryShell.js +55 -15
  29. package/dist/react/media/DenseGalleryShell.js.map +1 -1
  30. package/dist/react/media/FocusViewer.d.ts.map +1 -1
  31. package/dist/react/media/FocusViewer.js +14 -14
  32. package/dist/react/media/FocusViewer.js.map +1 -1
  33. package/dist/react/media/FrameGrabber.d.ts +23 -0
  34. package/dist/react/media/FrameGrabber.d.ts.map +1 -1
  35. package/dist/react/media/FrameGrabber.js +67 -1
  36. package/dist/react/media/FrameGrabber.js.map +1 -1
  37. package/dist/react/media/MediaResize.d.ts +51 -19
  38. package/dist/react/media/MediaResize.d.ts.map +1 -1
  39. package/dist/react/media/MediaResize.js +20 -13
  40. package/dist/react/media/MediaResize.js.map +1 -1
  41. package/dist/react/media/VideoChapterEditor.d.ts +67 -0
  42. package/dist/react/media/VideoChapterEditor.d.ts.map +1 -0
  43. package/dist/react/media/VideoChapterEditor.js +148 -0
  44. package/dist/react/media/VideoChapterEditor.js.map +1 -0
  45. package/dist/react/media/VideoPlayer.d.ts +46 -4
  46. package/dist/react/media/VideoPlayer.d.ts.map +1 -1
  47. package/dist/react/media/VideoPlayer.js +124 -11
  48. package/dist/react/media/VideoPlayer.js.map +1 -1
  49. package/dist/react/media/VideoTurner.d.ts +15 -2
  50. package/dist/react/media/VideoTurner.d.ts.map +1 -1
  51. package/dist/react/media/VideoTurner.js +29 -2
  52. package/dist/react/media/VideoTurner.js.map +1 -1
  53. package/dist/react/media/denseGallery.css +104 -46
  54. package/dist/react/media/hlsSource.d.ts +57 -0
  55. package/dist/react/media/hlsSource.d.ts.map +1 -1
  56. package/dist/react/media/hlsSource.js +177 -1
  57. package/dist/react/media/hlsSource.js.map +1 -1
  58. package/dist/react/media/index.d.ts +2 -0
  59. package/dist/react/media/index.d.ts.map +1 -1
  60. package/dist/react/media/index.js +6 -0
  61. package/dist/react/media/index.js.map +1 -1
  62. package/dist/react/media/mediaResize.css +127 -54
  63. package/dist/react/media/pictureEditor.css +52 -6
  64. package/dist/react/media/videoChapterEditor.css +203 -0
  65. package/dist/react/media/videoChapters.d.ts +117 -0
  66. package/dist/react/media/videoChapters.d.ts.map +1 -0
  67. package/dist/react/media/videoChapters.js +95 -0
  68. package/dist/react/media/videoChapters.js.map +1 -0
  69. package/dist/react/media-gallery/GalleryTable.d.ts +52 -1
  70. package/dist/react/media-gallery/GalleryTable.d.ts.map +1 -1
  71. package/dist/react/media-gallery/GalleryTable.js +40 -9
  72. package/dist/react/media-gallery/GalleryTable.js.map +1 -1
  73. package/dist/react/media-gallery/MediaGallery.d.ts +107 -4
  74. package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
  75. package/dist/react/media-gallery/MediaGallery.js +484 -104
  76. package/dist/react/media-gallery/MediaGallery.js.map +1 -1
  77. package/dist/react/media-gallery/MediaMetaEditor.d.ts +48 -0
  78. package/dist/react/media-gallery/MediaMetaEditor.d.ts.map +1 -0
  79. package/dist/react/media-gallery/MediaMetaEditor.js +66 -0
  80. package/dist/react/media-gallery/MediaMetaEditor.js.map +1 -0
  81. package/dist/react/media-gallery/index.d.ts +2 -0
  82. package/dist/react/media-gallery/index.d.ts.map +1 -1
  83. package/dist/react/media-gallery/index.js +5 -0
  84. package/dist/react/media-gallery/index.js.map +1 -1
  85. package/dist/react/media-gallery/mediaGallery.css +180 -4
  86. package/dist/react/media-gallery/mediaMeta.d.ts +62 -0
  87. package/dist/react/media-gallery/mediaMeta.d.ts.map +1 -0
  88. package/dist/react/media-gallery/mediaMeta.js +56 -0
  89. package/dist/react/media-gallery/mediaMeta.js.map +1 -0
  90. package/dist/react/media-gallery/playbackPreferences.d.ts +77 -0
  91. package/dist/react/media-gallery/playbackPreferences.d.ts.map +1 -1
  92. package/dist/react/media-gallery/playbackPreferences.js +42 -0
  93. package/dist/react/media-gallery/playbackPreferences.js.map +1 -1
  94. package/dist/react/media-gallery/types.d.ts +34 -0
  95. package/dist/react/media-gallery/types.d.ts.map +1 -1
  96. package/dist/scripts/guardrailsEnforce.d.ts +29 -0
  97. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -0
  98. package/dist/styles-areas/activity-bar.css +30 -0
  99. package/dist/styles-areas/analytics.css +672 -0
  100. package/dist/styles-areas/auth.css +154 -0
  101. package/dist/styles-areas/calendar.css +416 -0
  102. package/dist/styles-areas/chart-table.css +41 -0
  103. package/dist/styles-areas/charts.css +89 -0
  104. package/dist/styles-areas/chat.css +364 -0
  105. package/dist/styles-areas/clipboard.css +26 -0
  106. package/dist/styles-areas/code-editor.css +130 -0
  107. package/dist/styles-areas/companion-link.css +70 -0
  108. package/dist/styles-areas/core.css +2503 -0
  109. package/dist/styles-areas/dashboard-grid.css +84 -0
  110. package/dist/styles-areas/data-table.css +657 -0
  111. package/dist/styles-areas/deep-link.css +26 -0
  112. package/dist/styles-areas/diff-viewer.css +30 -0
  113. package/dist/styles-areas/disk-usage.css +262 -0
  114. package/dist/styles-areas/emoji.css +154 -0
  115. package/dist/styles-areas/fields.css +582 -0
  116. package/dist/styles-areas/file-tree.css +33 -0
  117. package/dist/styles-areas/filter-rail.css +235 -0
  118. package/dist/styles-areas/folder-tree.css +219 -0
  119. package/dist/styles-areas/keep-awake.css +26 -0
  120. package/dist/styles-areas/layout-engine.css +239 -0
  121. package/dist/styles-areas/markdown.css +87 -0
  122. package/dist/styles-areas/master-detail.css +239 -0
  123. package/dist/styles-areas/master-lock.css +26 -0
  124. package/dist/styles-areas/media-gallery.css +351 -0
  125. package/dist/styles-areas/media.css +658 -0
  126. package/dist/styles-areas/nav.css +488 -0
  127. package/dist/styles-areas/notifications.css +245 -0
  128. package/dist/styles-areas/overlays.css +133 -0
  129. package/dist/styles-areas/palette.css +140 -0
  130. package/dist/styles-areas/pdf-viewer.css +47 -0
  131. package/dist/styles-areas/problem-detail.css +97 -0
  132. package/dist/styles-areas/rich-text.css +167 -0
  133. package/dist/styles-areas/sharing.css +153 -0
  134. package/dist/styles-areas/spreadsheet.css +37 -0
  135. package/dist/styles-areas/stats.css +281 -0
  136. package/dist/styles-areas/test-report.css +133 -0
  137. package/dist/styles-areas/virtual.css +30 -0
  138. package/dist/styles-areas/wizard.css +381 -0
  139. package/dist/styles-areas/workbench.css +543 -0
  140. package/dist/styles-areas/workbook-viewer.css +44 -0
  141. package/dist/styles-static.css +34 -23
  142. package/dist/styles.css +34 -23
  143. package/package.json +111 -31
  144. package/scripts/checkDistExports.ts +67 -0
  145. package/scripts/cssRules.ts +125 -0
  146. package/scripts/fixtureAppCss.ts +344 -0
  147. package/scripts/generateAreaStyles.ts +253 -0
  148. package/scripts/guardrailsEnforce.spec.ts +62 -0
  149. package/scripts/guardrailsEnforce.ts +67 -3
  150. package/scripts/styleAreas.ts +585 -0
  151. package/scripts/verify.ts +5 -0
  152. package/src/barrelsReachNoOptionalPeer.spec.ts +117 -9
  153. package/src/docsMatchTheSplit.spec.ts +98 -0
  154. package/src/fixtureAppCss.spec.ts +177 -0
  155. package/src/namedSubpathsResolve.spec.ts +35 -0
  156. package/src/publishShape.spec.ts +67 -0
  157. package/src/react/components/ListToolbar.spec.tsx +172 -0
  158. package/src/react/components/ListToolbar.tsx +151 -0
  159. package/src/react/file-tree/FileTree.spec.tsx +1009 -0
  160. package/src/react/file-tree/FileTree.tsx +1459 -309
  161. package/src/react/file-tree/fileTree.css +151 -0
  162. package/src/react/file-tree/fileTreeSelection.spec.ts +327 -0
  163. package/src/react/file-tree/fileTreeSelection.ts +321 -0
  164. package/src/react/file-tree/index.ts +21 -0
  165. package/src/react/index.ts +1 -0
  166. package/src/react/lib/form.ts +51 -8
  167. package/src/react/media/DenseGalleryShell.tsx +108 -36
  168. package/src/react/media/FocusViewer.tsx +15 -8
  169. package/src/react/media/FrameGrabber.spec.tsx +175 -0
  170. package/src/react/media/FrameGrabber.tsx +67 -0
  171. package/src/react/media/MediaResize.spec.tsx +84 -14
  172. package/src/react/media/MediaResize.tsx +51 -19
  173. package/src/react/media/VideoChapterEditor.tsx +428 -0
  174. package/src/react/media/VideoPlayer.spec.tsx +111 -1
  175. package/src/react/media/VideoPlayer.tsx +158 -11
  176. package/src/react/media/VideoTurner.spec.tsx +67 -0
  177. package/src/react/media/VideoTurner.tsx +70 -15
  178. package/src/react/media/denseGallery.css +104 -46
  179. package/src/react/media/hlsSource.recovery.spec.ts +213 -0
  180. package/src/react/media/hlsSource.spec.ts +8 -0
  181. package/src/react/media/hlsSource.ts +189 -1
  182. package/src/react/media/index.ts +14 -0
  183. package/src/react/media/mediaResize.css +127 -54
  184. package/src/react/media/pictureEditor.css +52 -6
  185. package/src/react/media/videoChapterEditor.css +203 -0
  186. package/src/react/media/videoChapters.spec.ts +176 -0
  187. package/src/react/media/videoChapters.ts +181 -0
  188. package/src/react/media-gallery/GalleryTable.tsx +93 -3
  189. package/src/react/media-gallery/MediaGallery.spec.tsx +1156 -10
  190. package/src/react/media-gallery/MediaGallery.tsx +813 -95
  191. package/src/react/media-gallery/MediaMetaEditor.tsx +237 -0
  192. package/src/react/media-gallery/galleryTable.spec.ts +41 -1
  193. package/src/react/media-gallery/index.ts +7 -0
  194. package/src/react/media-gallery/mediaGallery.css +180 -4
  195. package/src/react/media-gallery/mediaMeta.spec.ts +95 -0
  196. package/src/react/media-gallery/mediaMeta.ts +101 -0
  197. package/src/react/media-gallery/playbackPreferences.spec.ts +172 -0
  198. package/src/react/media-gallery/playbackPreferences.ts +100 -0
  199. package/src/react/media-gallery/types.ts +34 -0
  200. package/src/shippedFilesAreTracked.spec.ts +69 -0
  201. package/src/styles-areas/activity-bar.css +30 -0
  202. package/src/styles-areas/analytics.css +672 -0
  203. package/src/styles-areas/auth.css +154 -0
  204. package/src/styles-areas/calendar.css +416 -0
  205. package/src/styles-areas/chart-table.css +41 -0
  206. package/src/styles-areas/charts.css +89 -0
  207. package/src/styles-areas/chat.css +364 -0
  208. package/src/styles-areas/clipboard.css +26 -0
  209. package/src/styles-areas/code-editor.css +130 -0
  210. package/src/styles-areas/companion-link.css +70 -0
  211. package/src/styles-areas/core.css +2503 -0
  212. package/src/styles-areas/dashboard-grid.css +84 -0
  213. package/src/styles-areas/data-table.css +657 -0
  214. package/src/styles-areas/deep-link.css +26 -0
  215. package/src/styles-areas/diff-viewer.css +30 -0
  216. package/src/styles-areas/disk-usage.css +262 -0
  217. package/src/styles-areas/emoji.css +154 -0
  218. package/src/styles-areas/fields.css +582 -0
  219. package/src/styles-areas/file-tree.css +33 -0
  220. package/src/styles-areas/filter-rail.css +235 -0
  221. package/src/styles-areas/folder-tree.css +219 -0
  222. package/src/styles-areas/keep-awake.css +26 -0
  223. package/src/styles-areas/layout-engine.css +239 -0
  224. package/src/styles-areas/markdown.css +87 -0
  225. package/src/styles-areas/master-detail.css +239 -0
  226. package/src/styles-areas/master-lock.css +26 -0
  227. package/src/styles-areas/media-gallery.css +351 -0
  228. package/src/styles-areas/media.css +658 -0
  229. package/src/styles-areas/nav.css +488 -0
  230. package/src/styles-areas/notifications.css +245 -0
  231. package/src/styles-areas/overlays.css +133 -0
  232. package/src/styles-areas/palette.css +140 -0
  233. package/src/styles-areas/pdf-viewer.css +47 -0
  234. package/src/styles-areas/problem-detail.css +97 -0
  235. package/src/styles-areas/rich-text.css +167 -0
  236. package/src/styles-areas/sharing.css +153 -0
  237. package/src/styles-areas/spreadsheet.css +37 -0
  238. package/src/styles-areas/stats.css +281 -0
  239. package/src/styles-areas/test-report.css +133 -0
  240. package/src/styles-areas/virtual.css +30 -0
  241. package/src/styles-areas/wizard.css +381 -0
  242. package/src/styles-areas/workbench.css +543 -0
  243. package/src/styles-areas/workbook-viewer.css +44 -0
  244. package/src/styles-static.css +34 -23
  245. package/src/styles.css +34 -23
  246. package/src/stylesAreas.spec.ts +247 -0
  247. package/src/stylesUtilitiesMatches.spec.ts +6 -77
  248. package/src/testFilesRunInParallel.spec.ts +79 -0
  249. package/src/typecheckCachesAreSeparate.spec.ts +128 -0
  250. package/src/verifyGraph.spec.ts +6 -0
@@ -9,44 +9,103 @@
9
9
  * and the owner reported the feature as absent while prod was serving it. So both grips are
10
10
  * legible without hover, and both brighten further on hover/focus rather than appearing on
11
11
  * it. Do not "quieten" either back to a hover-only affordance.
12
+ *
13
+ * ── 🔴 2026-09-16: the grips live OUTSIDE the picture ──────────────────────────────────
14
+ * Owner, `ideas/collections-09-14.md`: *"Fix the drag to resize area to no longer overlap
15
+ * part of the horizontal play timeline area that shows what part of the video is playing. It
16
+ * should only be draggable when my mouse pointer is at the bottom right corner of the video
17
+ * player, just like mac windows for other apps work."*
18
+ *
19
+ * The corner grip used to be a 64×64 overlay `position: absolute; right: 0; bottom: 0` INSIDE
20
+ * the clipped frame, riding at `z-index: 12` over Vidstack's control layer, and the gallery
21
+ * reserved a matching `padding-inline-end` gutter in the player's last two control groups so
22
+ * the buttons would not lay out underneath it.
23
+ *
24
+ * 🔴 MEASURED before anything was changed, with that geometry restored in a real browser
25
+ * (`e2e/resize-grip-rail.spec.ts` has the numbers): the gutter DID keep the corner's rectangle
26
+ * off `.vds-time-slider`'s. It is not the thing that was broken, and widening it would have
27
+ * fixed nothing. Three things were, and none of them is reachable from a gutter:
28
+ *
29
+ * 1. `.cbmr-bar` — a full-width strip under the frame whose `padding: 11px 0` with
30
+ * `margin-block: -11px` put 11px of drag surface back inside the picture, spanning the
31
+ * whole scrub bar. Horizontal padding is BESIDE the slider; the bar is UNDER it.
32
+ * 2. A gutter can only shorten a control bar this package styles. A `nativeVideoStage`
33
+ * gallery puts a plain `<video controls>` in that frame and its UA bar is full-width and
34
+ * untouchable, so a grip in the corner lands on the native timeline with no recourse.
35
+ * 3. It was paid for in the thing the owner was complaining about: 64px of scrub bar,
36
+ * surrendered so the grip could sit beside it.
37
+ *
38
+ * So the grips moved off the picture entirely, into `.cbmr-rail` — a strip of chrome BELOW the
39
+ * frame, which is where a Mac window's resize corner is: the window's outside edge, not a
40
+ * patch of its content. Nothing of either grip is inside the frame at any size, and that is
41
+ * the only shape in which "does not overlap the timeline" is a property of the layout rather
42
+ * than a tuning against whatever the current player happens to draw. The gutter rule in
43
+ * `denseGallery.css` went with it, so the time slider is full width again — which is the rest
44
+ * of the same sentence.
12
45
  */
13
46
 
14
47
  /*
15
- * 🔴 ONE number, read from two files.
48
+ * 🔴 ONE number: the grip's reach AND the height of the rail that holds it.
16
49
  *
17
- * The corner grip's size is also the width of the gutter `denseGallery.css` reserves at the
18
- * end of a video player's control bar, so the player's own buttons never lay out underneath
19
- * it. Two literals would desync on the first tweak and the symptom — a grip that covers the
20
- * fullscreen button, or a gutter with nothing in it — is a thing you can only see by hand.
21
- * Declared on `:root` because the gutter rule applies to an ANCESTOR of the grip, and a
22
- * custom property set on the grip itself does not reach back up the tree.
50
+ * 64px is the owner's 2026-08-25 *"make the corner expansion grabbable area bigger … for some
51
+ * reason it's hard to grab"* and it may not go down — see `.cbmr-corner`. The rail is exactly
52
+ * that tall because the rail exists to hold the grip; two literals would desync on the first
53
+ * tweak and the symptom is a grip clipped by its own strip, which only shows up by hand.
54
+ * Declared on `:root` because the coarse-pointer block at the bottom raises it for both.
23
55
  */
24
56
  :root {
25
57
  --cbmr-corner-size: 64px;
26
58
  }
27
59
 
60
+ /* ── The rail: the chrome the grips live in ──────────────────────────────────────────── */
61
+
62
+ /*
63
+ * 🔴 A sibling of the frame, never a child of it. Every consumer renders it directly after
64
+ * the picture's box (`DenseGalleryShell`, `FocusViewer`), and `MediaResize.spec.tsx` fails if
65
+ * a player stops doing so — a grip that drifts back inside the frame is exactly the
66
+ * 2026-09-14 report, and it is invisible to everything except a rendered rectangle.
67
+ *
68
+ * It is the positioning context for the corner, which is what keeps the grip's 64px box
69
+ * bounded by this strip instead of hanging up over the picture.
70
+ */
71
+ .cbmr-rail {
72
+ position: relative;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ inline-size: 100%;
77
+ block-size: var(--cbmr-corner-size, 64px);
78
+ /* Both consumers stack it in a flex COLUMN (`.cbgd-watch-player`, `.inspector`), where a
79
+ default `flex-shrink: 1` would let a short container squeeze the strip and clip the grip
80
+ out of its own bottom-right corner. The picture is what gives way, never the handle. */
81
+ flex: none;
82
+ }
83
+
28
84
  /* ── The corner ─────────────────────────────────────────────────────────────────────── */
29
85
 
30
86
  .cbmr-corner {
31
87
  position: absolute;
32
- right: 0;
33
- bottom: 0;
88
+ inset-inline-end: 0;
89
+ inset-block-end: 0;
34
90
  /*
35
91
  * 🔴 ABOVE the video player's control layer, which is `z-index: 10` (Vidstack's
36
- * `[data-view-type='video'] .vds-controls`) in THIS same stacking context —
37
- * `[data-media-player]` sets `contain: style`, which opens no stacking context, so
38
- * nothing isolates the player's chrome from the frame around it. At the old `3` the
39
- * grip was painted over and, worse, click-blocked by the bottom `.vds-controls-group`
40
- * (`width: 100%`, `pointer-events: auto`) whenever the bar was visible — which is on
41
- * hover, which is precisely when a person reaches for it. That is the owner's
42
- * 2026-08-25 *"for some reason it's hard to grab"*, and it is why this number may not
43
- * go back down without also moving the grip out of the frame.
92
+ * `[data-view-type='video'] .vds-controls`) — `[data-media-player]` sets `contain:
93
+ * style`, which opens no stacking context, so nothing isolates the player's chrome
94
+ * from the frame around it. At the old `3` the grip was painted over and, worse,
95
+ * click-blocked by the bottom `.vds-controls-group` (`width: 100%`, `pointer-events:
96
+ * auto`) whenever the bar was visible — which is on hover, which is precisely when a
97
+ * person reaches for it. That is the owner's 2026-08-25 *"for some reason it's hard to
98
+ * grab"*, and it is why this number may not go back down.
99
+ *
100
+ * Since 2026-09-16 the grip is in `.cbmr-rail`, outside the frame, so it no longer has
101
+ * to win that fight — but a player whose control layer grows a `bottom: -Npx` overhang
102
+ * would re-open it, and 12 costs nothing. Keep it.
44
103
  *
45
104
  * `12` and not more: it must stay UNDER Vidstack's menus (50) and its fullscreen
46
105
  * surface, so opening the settings cog still covers the grip rather than the grip
47
106
  * punching a hole through an open menu.
48
107
  *
49
- * LOCAL stacking only. This lifts the grip inside its own positioned frame and must
108
+ * LOCAL stacking only. This lifts the grip inside its own positioned rail and must
50
109
  * never reach the app-wide bands — a `--z-` band here would raise the grip over the
51
110
  * sticky header and every overlay above it.
52
111
  * guardrails-ignore no-raw-zindex: local stacking inside a positioned media frame. */
@@ -54,35 +113,37 @@
54
113
  display: flex;
55
114
  align-items: flex-end;
56
115
  justify-content: flex-end;
57
- /* The target, and the gutter the control bar leaves for it — see `--cbmr-corner-size`.
58
- Well over DESIGN.md §5's 44px floor, deliberately: the owner asked for a bigger grab
59
- area in the same breath as reporting he could not hit it. The padding is the reach,
60
- the glyph is the mark. */
116
+ /* The target. Well over DESIGN.md §5's 44px floor, deliberately: the owner asked for a
117
+ bigger grab area in the same breath as reporting he could not hit it. The padding is
118
+ the reach, the glyph is the mark. It fills the rail exactly — see `--cbmr-corner-size`. */
61
119
  inline-size: var(--cbmr-corner-size, 64px);
62
120
  block-size: var(--cbmr-corner-size, 64px);
63
- padding: 0 4px 4px 0;
121
+ padding: 0 6px 6px 0;
64
122
  box-sizing: border-box;
65
123
  cursor: nwse-resize;
66
124
  touch-action: none;
67
125
  border: 0;
68
126
  background: none;
69
- /* A soft scrim rather than a filled chip: the grip must be legible over a white sky and
70
- over black letterboxing, and a corner-anchored gradient does that without drawing a
71
- box on top of the picture. */
72
- border-end-end-radius: inherit;
73
127
  }
74
128
 
129
+ /*
130
+ * The hover plate.
131
+ *
132
+ * 🔴 A TOKEN, where this used to be a hardcoded black radial scrim. The scrim was right while
133
+ * the grip rode on arbitrary media — a photograph has no theme, so app colours tinted it. In
134
+ * the rail the grip rides on app chrome like every other control, so it takes the app's
135
+ * colours and the two `guardrails-ignore no-hardcoded-colors` suppressions this rule used to
136
+ * need are gone with it.
137
+ */
75
138
  .cbmr-corner::before {
76
139
  content: '';
77
140
  position: absolute;
78
- inset: 0;
79
- border-end-end-radius: inherit;
80
- /* Black, not a surface token, and for the same reason `.inspector__viewport` and
81
- the gallery's action pills hardcode theirs: this scrim rides on ARBITRARY MEDIA, which has no
82
- theme. A token here would tint the grip with app chrome over somebody's photograph.
83
- guardrails-ignore no-hardcoded-colors: see above. */
84
- /* guardrails-ignore no-hardcoded-colors: one line, because the excuse covers the LINE. */
85
- background: radial-gradient(circle at 100% 100%, rgb(0 0 0 / 0.55) 0%, rgb(0 0 0 / 0.34) 52%, rgb(0 0 0 / 0) 80%);
141
+ inset-inline-end: 2px;
142
+ inset-block-end: 2px;
143
+ inline-size: 30px;
144
+ block-size: 30px;
145
+ border-radius: var(--radius-md, 0.5rem);
146
+ background: transparent;
86
147
  pointer-events: none;
87
148
  transition: background 120ms ease;
88
149
  }
@@ -93,10 +154,9 @@
93
154
  block-size: 16px;
94
155
  overflow: visible;
95
156
  fill: none;
96
- /* White, not a token: this rides on arbitrary media, which has no theme — the same
97
- reasoning the stage's overlay pills use.
98
- guardrails-ignore no-hardcoded-colors: see above. */
99
- stroke: rgb(255 255 255 / 0.92);
157
+ /* Legible at rest — see the file header. `--muted-foreground`, never `--border`, which is
158
+ the colour of a hairline nobody is meant to look at. */
159
+ stroke: var(--muted-foreground, currentColor);
100
160
  stroke-width: 1.6;
101
161
  stroke-linecap: round;
102
162
  transition: stroke 120ms ease, transform 120ms ease;
@@ -104,15 +164,13 @@
104
164
 
105
165
  .cbmr-corner:hover .cbmr-corner-glyph,
106
166
  .cbmr-corner:focus-visible .cbmr-corner-glyph {
107
- /* guardrails-ignore no-hardcoded-colors: the hover state of the white glyph above. */
108
- stroke: rgb(255 255 255 / 1);
167
+ stroke: var(--foreground, var(--muted-foreground));
109
168
  transform: scale(1.15);
110
169
  }
111
170
 
112
171
  .cbmr-corner:hover::before,
113
172
  .cbmr-corner:focus-visible::before {
114
- /* guardrails-ignore no-hardcoded-colors: the hover state of the scrim above. */
115
- background: radial-gradient(circle at 100% 100%, rgb(0 0 0 / 0.7) 0%, rgb(0 0 0 / 0.45) 45%, rgb(0 0 0 / 0) 75%);
173
+ background: var(--border);
116
174
  }
117
175
 
118
176
  .cbmr-corner:focus-visible {
@@ -122,18 +180,27 @@
122
180
 
123
181
  /* ── The bottom edge ────────────────────────────────────────────────────────────────── */
124
182
 
183
+ /*
184
+ * 🔴 COARSE POINTERS ONLY, and that is a decision, not a tidy-up — see `MediaResize.tsx`'s
185
+ * header for the whole of it. *"It should only be draggable when my mouse pointer is at the
186
+ * bottom right corner"* is a verdict on this bar as much as on the corner's old position; the
187
+ * one reason it was ever kept beside the corner is about a THUMB, so it survives exactly where
188
+ * a thumb is.
189
+ */
125
190
  .cbmr-bar {
191
+ display: none;
126
192
  inline-size: 100%;
127
193
  block-size: 22px;
128
- display: flex;
129
194
  align-items: center;
130
195
  justify-content: center;
131
196
  cursor: ns-resize;
132
197
  touch-action: none;
133
- /* The generous half of the 44px target, without 44px of visible gutter. */
198
+ /* The generous half of the 44px target, without 44px of visible gutter. 🔴 No negative
199
+ margin pulling it back out of the rail: that is how invisible padding came to hang over
200
+ the picture's bottom edge and over the transport below it, which is the overlap the
201
+ 2026-09-14 report is about. The rail is tall enough to hold the reach. */
134
202
  padding: 11px 0;
135
203
  box-sizing: content-box;
136
- margin-block: -11px;
137
204
  border: 0;
138
205
  background: none;
139
206
  }
@@ -172,15 +239,17 @@
172
239
  /*
173
240
  * ── A finger is not a mouse ──────────────────────────────────────────────────────────
174
241
  * Owner, 2026-08-25 and again 2026-09-08: *"make the corner expansion grabbable area
175
- * bigger … for some reason it's hard to grab"*. The z-index half of that is above; this is
176
- * the other half, and it is the one a desktop measurement cannot see. 64px clears
177
- * `DESIGN.md` §5's 44px floor for a POINTER; a fingertip's contact patch is ~9mm, which on a
178
- * phone is most of that box, and the grip sits in the one corner a thumb reaches across the
179
- * whole screen to get to.
242
+ * bigger … for some reason it's hard to grab"*. 64px clears `DESIGN.md` §5's 44px floor for a
243
+ * POINTER; a fingertip's contact patch is ~9mm, which on a phone is most of that box, and the
244
+ * grip sits in the one corner a thumb reaches across the whole screen to get to.
180
245
  *
181
246
  * Coarse pointers only, so the desktop look — which the owner explicitly said was good — is
182
- * untouched. `--cbmr-corner-size` is read by `denseGallery.css` for the gutter it reserves in
183
- * the player's control bar, so widening it here widens that too and the two cannot desync.
247
+ * untouched. Raising `--cbmr-corner-size` raises the rail with it, so the bigger grip still
248
+ * fits inside its strip rather than growing back up over the picture.
249
+ *
250
+ * 🔴 And this is where `.cbmr-bar` exists at all: the corner is a small target for a thumb,
251
+ * and on a portrait clip the picture's bottom edge is the one part of the stage whose position
252
+ * does not move with the file.
184
253
  */
185
254
  @media (pointer: coarse) {
186
255
  :root {
@@ -192,9 +261,13 @@
192
261
  inline-size: 22px;
193
262
  block-size: 22px;
194
263
  }
264
+ .cbmr-corner::before {
265
+ inline-size: 40px;
266
+ block-size: 40px;
267
+ }
195
268
  /* Same reasoning for the bar: the ink stays a hairline, the reach grows. */
196
269
  .cbmr-bar {
270
+ display: flex;
197
271
  padding: 15px 0;
198
- margin-block: -15px;
199
272
  }
200
273
  }
@@ -263,12 +263,58 @@
263
263
  cursor: default;
264
264
  }
265
265
 
266
- /* A quarter-turned clip is still laid out in its ORIGINAL box, so its width is bounded by the
267
- container's HEIGHT and vice versa. Container query units are the only way to say that. Set
268
- only for the ODD turns — a half turn does not change the box and pays for none of this. */
269
- .cbpe-frame-film[data-turned='true'] video {
266
+ /* The film, sized exactly like the `<img>` the picture editor puts in the same frame: contained,
267
+ centred, never overflowing the box it is previewed in. Written out here because `.cbpe-frame
268
+ img` cannot say it for an element that is not an image, and without it the preview is whatever
269
+ size the browser's default happens to make it — which for a phone clip is a postage stamp in
270
+ the middle of a black frame, i.e. the thing this dialog is for is invisible. */
271
+ .cbpe-frame video {
272
+ max-width: 100%;
273
+ max-height: 100%;
274
+ object-fit: contain;
275
+ transform-origin: center;
276
+ }
277
+
278
+ /* A quarter-turned clip is still laid out in its ORIGINAL box, so its width has to be read from
279
+ the container's HEIGHT and vice versa. Container query units are the only way to say that.
280
+ Set only for the ODD turns — a half turn does not change the box and pays for none of this.
281
+
282
+ 🔴 An explicit SIZE, not a `max-*` bound. The same correction, for the same reason, as
283
+ `.cbmg-stage-turn[data-turned='true']` in `mediaGallery.css` (which documents the measurement):
284
+ a bound can only shrink, so a turned clip stayed whatever size it already was and the preview
285
+ showed a pillar the stage was about to show full-width. The preview and the stage have to
286
+ agree — a preview that under-sells the turn is a preview somebody rejects a correct edit on.
287
+ `img` as well as `video`: a clip that will not play falls back to its poster, and the poster
288
+ is turned by the same transform. */
289
+ .cbpe-frame-film[data-turned='true'] video,
290
+ .cbpe-frame-film[data-turned='true'] img {
270
291
  max-width: none;
271
292
  max-height: none;
272
- max-block-size: 100cqi;
273
- max-inline-size: 100cqb;
293
+ max-block-size: none;
294
+ max-inline-size: none;
295
+ width: 100cqb;
296
+ height: 100cqi;
297
+ aspect-ratio: auto;
298
+ object-fit: contain;
299
+ }
300
+
301
+ /* Why there is no moving picture, said ON the frame that would otherwise be black — the owner's
302
+ *"I see a black screen"*, which he could only report as a black screen because the dialog said
303
+ nothing. Centred over the ground rather than under the frame so it is read as being about the
304
+ picture, and `pointer-events: none` so it never eats a press meant for the film. */
305
+ .cbpe-frame-note {
306
+ position: absolute;
307
+ inset-inline: 1.5rem;
308
+ top: 50%;
309
+ transform: translateY(-50%);
310
+ margin: 0;
311
+ text-align: center;
312
+ text-wrap: balance;
313
+ font-size: 13px;
314
+ line-height: 1.5;
315
+ /* guardrails-ignore no-hardcoded-colors: sits ON the picture ground, which is true black in
316
+ both themes — the same argument as `.cbpe-hint` above. */
317
+ color: rgb(255 255 255 / 0.82);
318
+ text-shadow: 0 1px 2px rgb(0 0 0 / 0.6);
319
+ pointer-events: none;
274
320
  }
@@ -0,0 +1,203 @@
1
+ /* Plain CSS, colocated with `VideoChapterEditor.tsx` and imported by it — the same
2
+ convention, and the same reason, as `videoCutEditor.css`: the editor is lazily imported
3
+ from the gallery, so these bytes ride the gallery's chunk instead of landing in whichever
4
+ ENTRY stylesheet happened to scan a utility class. Every colour is a theme token. */
5
+
6
+ .cbch {
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: 0.75rem;
10
+ min-height: 0;
11
+ }
12
+
13
+ .cbch-video {
14
+ inline-size: 100%;
15
+ max-block-size: 44vh;
16
+ background: var(--surface-2, var(--background));
17
+ border-radius: var(--radius-md, 8px);
18
+ }
19
+
20
+ /* The one authoring row: name the section, then mark it. In that order, because the button
21
+ is disabled until there is a name — see the component. */
22
+ .cbch-add {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 0.5rem;
26
+ flex-wrap: wrap;
27
+ }
28
+
29
+ .cbch-add-field {
30
+ flex: 1 1 14rem;
31
+ min-inline-size: 0;
32
+ }
33
+
34
+ /* Tabular figures, and this is not cosmetic: the playhead readout changes ten times a second,
35
+ and proportional digits make the whole row shuffle sideways under the controls beside it
36
+ while a clip plays. */
37
+ .cbch-playhead {
38
+ font-variant-numeric: tabular-nums;
39
+ font-size: 0.875rem;
40
+ color: var(--muted-foreground);
41
+ }
42
+
43
+ /* ── The strip ───────────────────────────────────────────────────────────────
44
+ What the scrub bar will look like once this is saved: the named sections, in place and to
45
+ scale. Not a scrub bar itself — the `<video>`'s own controls are that, and a second
46
+ draggable timeline would be two controls fighting for one gesture. */
47
+ .cbch-strip {
48
+ position: relative;
49
+ display: flex;
50
+ list-style: none;
51
+ margin: 0;
52
+ padding: 0;
53
+ inline-size: 100%;
54
+ block-size: 40px;
55
+ border-radius: var(--radius-md, 8px);
56
+ background: var(--surface-3, var(--surface-2));
57
+ border: 1px solid var(--border);
58
+ overflow: hidden;
59
+ }
60
+
61
+ /* The list item carries the WIDTH — the section's duration — and the button fills it. Split
62
+ in two so the width lives on the flex child rather than on a control that also has padding. */
63
+ .cbch-span-item {
64
+ min-inline-size: 0;
65
+ flex: 0 0 auto;
66
+ block-size: 100%;
67
+ }
68
+
69
+ /* The unnamed opening, when the first chapter is not at 0:00. Drawn, but not a control: there
70
+ is nothing to jump to and nothing to rename — it is the part that has no section yet. */
71
+ .cbch-lead {
72
+ flex: 0 0 auto;
73
+ block-size: 100%;
74
+ border-inline-end: 1px solid var(--border);
75
+ background: repeating-linear-gradient(
76
+ 135deg,
77
+ transparent 0 6px,
78
+ color-mix(in srgb, var(--muted-foreground) 14%, transparent) 6px 12px
79
+ );
80
+ }
81
+
82
+ .cbch-span {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 0.25rem;
86
+ /* `min-inline-size: 0` and no flex-grow: the inline width set per section is the truth. */
87
+ inline-size: 100%;
88
+ block-size: 100%;
89
+ padding-inline: 0.375rem;
90
+ border: 0;
91
+ /* A hairline between sections, so two adjacent ones read as two rather than one. */
92
+ border-inline-end: 1px solid var(--border);
93
+ background: var(--surface-1, var(--background));
94
+ color: var(--foreground);
95
+ font-size: 0.75rem;
96
+ white-space: nowrap;
97
+ overflow: hidden;
98
+ cursor: pointer;
99
+ transition: background 120ms ease;
100
+ }
101
+
102
+ .cbch-span-item:last-of-type .cbch-span {
103
+ border-inline-end: 0;
104
+ }
105
+
106
+ .cbch-span:hover {
107
+ background: var(--surface-2);
108
+ }
109
+
110
+ .cbch-span:focus-visible {
111
+ outline: 2px solid var(--ring, var(--muted-foreground));
112
+ outline-offset: -2px;
113
+ }
114
+
115
+ .cbch-span-label {
116
+ overflow: hidden;
117
+ text-overflow: ellipsis;
118
+ }
119
+
120
+ .cbch-strip-head {
121
+ position: absolute;
122
+ inset-block: 0;
123
+ inline-size: 2px;
124
+ background: var(--foreground);
125
+ /* Over the sections, but it must never eat a click meant for one. */
126
+ pointer-events: none;
127
+ }
128
+
129
+ .cbch-summary {
130
+ font-size: 0.875rem;
131
+ color: var(--muted-foreground);
132
+ margin: 0;
133
+ }
134
+
135
+ .cbch-list {
136
+ list-style: none;
137
+ margin: 0;
138
+ padding: 0;
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: 0.25rem;
142
+ max-block-size: 24vh;
143
+ /* guardrails-ignore no-raw-overflow: a Tailwind-free `.cbch-` list inside a lazily
144
+ imported editor, not a ScrollArea consumer — same as `videoCutEditor.css`. */
145
+ overflow-y: auto;
146
+ }
147
+
148
+ .cbch-row {
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 0.5rem;
152
+ padding-inline: 0.25rem;
153
+ border-radius: var(--radius-sm, 6px);
154
+ }
155
+
156
+ .cbch-row:hover {
157
+ background: var(--surface-2);
158
+ }
159
+
160
+ .cbch-row-goto {
161
+ flex: 0 0 auto;
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 0.375rem;
165
+ /* DESIGN.md §5's touch floor — a row you jump the playhead with is a real control. */
166
+ min-block-size: 44px;
167
+ padding-inline: 0.25rem;
168
+ border: 0;
169
+ background: none;
170
+ color: var(--muted-foreground);
171
+ font-variant-numeric: tabular-nums;
172
+ font-size: 0.875rem;
173
+ text-align: start;
174
+ cursor: pointer;
175
+ }
176
+
177
+ .cbch-row-goto:hover {
178
+ color: var(--foreground);
179
+ }
180
+
181
+ .cbch-row-goto:focus-visible {
182
+ outline: 2px solid var(--ring, var(--muted-foreground));
183
+ outline-offset: -2px;
184
+ border-radius: var(--radius-sm, 6px);
185
+ }
186
+
187
+ .cbch-row-title {
188
+ flex: 1 1 auto;
189
+ min-inline-size: 0;
190
+ }
191
+
192
+ .cbch-empty {
193
+ margin: 0;
194
+ padding-block: 0.75rem;
195
+ text-align: center;
196
+ font-size: 0.875rem;
197
+ color: var(--muted-foreground);
198
+ }
199
+
200
+ .cbch-note {
201
+ font-size: 0.875rem;
202
+ color: var(--muted-foreground);
203
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Marking points in a video and NAMING them — YouTube chapters, as a model, with no browser
3
+ * and no encoder in it.
4
+ *
5
+ * ── The owner's ask, 2026-09-15 ────────────────────────────────────────────────────────
6
+ * *"allow putting markers on videos at points in time and giving them a title for the section
7
+ * for knowing what that part of the video is about, like youtube chapters."*
8
+ *
9
+ * 🔴 This is the OTHER HALF of the gesture `videoCuts.ts` models, and it is deliberately not
10
+ * threaded through it. A cut is a point that says *remove what follows*; a chapter is a point
11
+ * that says *this part is called X, and it stays*. They look alike on a timeline and they have
12
+ * nothing else in common: a cut list is a REQUEST that costs a full re-encode and arrives back
13
+ * as a second file, while a chapter list is metadata that needs no encoder at all, applies the
14
+ * instant it is saved, and is free to be wrong. Putting a chapter on the cut list would make
15
+ * naming a section queue an ffmpeg job.
16
+ *
17
+ * ── Why the model is separate from the dialog ─────────────────────────────────────────
18
+ * Same reason as the cut list: a chapter list is DATA. It is produced by `VideoChapterEditor`,
19
+ * travels to whatever the app persists it in, and is read back by the player. A server that
20
+ * accepts one will want to re-check it, and "what makes a chapter list valid" must have exactly
21
+ * one definition or the editor and the store will disagree about a list neither of them
22
+ * refused. Nothing in this file imports React, and nothing in it touches the DOM.
23
+ *
24
+ * ── 🔴 Why it is HERE and not in `cursedbelt-core/media`, beside `videoCuts.ts` ────────
25
+ * Because `cursedbelt-core` is consumed BY PUBLISHED VERSION (forge rule 1; `src/noPathDeps.
26
+ * spec.ts` refuses a `file:`/`link:` dependency outright), so a module added to that package
27
+ * is unreachable from this one until it is published — `demo/vite.config.ts` says the same
28
+ * thing in its own header: *"a new subpath importable here the moment it is published"*. A
29
+ * chapter model in core today would mean this package could not typecheck against it, so the
30
+ * feature could not ship at all. The same reasoning, in the same words, put
31
+ * `fileTreeSelection.ts` in this package on 2026-09-15 — see `publicSurface.baseline`.
32
+ *
33
+ * It is written to MOVE: framework-free, no local imports but a type, and the only thing
34
+ * standing between it and `core/media/videoChapters.ts` is a published `cursedbelt-core`. When
35
+ * that happens the migration is a delete plus a re-export, exactly as `react/media/index.ts`
36
+ * already re-exports the cut model.
37
+ */
38
+ import type { PlayableVideo } from 'cursedbelt-core/media';
39
+ /** One named section, by the point in the source it BEGINS at. Seconds from the start. */
40
+ export interface VideoChapter {
41
+ startSeconds: number;
42
+ /** What that part is about. Trimmed, and never empty — an unnamed mark is unreadable. */
43
+ title: string;
44
+ }
45
+ /**
46
+ * The shortest section worth naming, in seconds — and therefore how far apart two chapters
47
+ * have to be to be two chapters.
48
+ *
49
+ * 🔴 It is the anti-duplicate rule and the anti-sliver rule at once, which is why there is one
50
+ * number rather than an equality test plus a floor. Two chapters at the same instant make a
51
+ * section of zero length: it is unreachable from the chapters menu, it renders as a divider
52
+ * with no room between it and the next, and on the scrub bar the two dividers land on the same
53
+ * pixel — so the list says two sections and the player can only ever show one. A second is
54
+ * generous (YouTube's own floor is ten) and still far below anything a person marks on purpose.
55
+ */
56
+ export declare const MIN_CHAPTER_SECONDS = 1;
57
+ /** A chapter with the end it runs to — the next one's start, or the end of the clip. */
58
+ export interface ChapterSpan extends VideoChapter {
59
+ endSeconds: number;
60
+ /**
61
+ * Stable across re-renders and safe as a React key.
62
+ *
63
+ * 🔴 The START, never the title. The editor renders a text field per row: keying on the
64
+ * title would give every keystroke a new key, so React would unmount the field being typed
65
+ * in and the caret would jump out of it on the first letter.
66
+ */
67
+ key: string;
68
+ }
69
+ /**
70
+ * A {@link PlayableVideo} that carries its named sections.
71
+ *
72
+ * 🔴 An intersection rather than a field on `PlayableVideo` itself for the reason in this
73
+ * file's header: that type lives in the published `cursedbelt-core`, which this package cannot
74
+ * edit and read in the same commit. It widens, so every existing consumer of `PlayableVideo` —
75
+ * every source, every stage, the player's own props — keeps typechecking untouched, and the
76
+ * day core carries `chapters` this alias becomes `PlayableVideo` and nothing else changes.
77
+ */
78
+ export type ChapteredVideo = PlayableVideo & {
79
+ chapters?: VideoChapter[];
80
+ };
81
+ /**
82
+ * Put a raw list into the one canonical form everything downstream may assume: inside the
83
+ * clip, ordered, named, and never two chapters closer than a section.
84
+ *
85
+ * Invalid entries are DROPPED rather than reported — this is the read path, and a list that
86
+ * came back from a store against a slightly different duration must still play. The editor
87
+ * refuses the same things out loud through {@link chapterProblem}, so nothing a person types
88
+ * is silently discarded; the two agree because the second is written in terms of the first.
89
+ *
90
+ * A non-finite start (the usual source is a playhead read from a `<video>` whose metadata has
91
+ * not loaded, which reports `NaN`) drops its chapter instead of poisoning the sort: every
92
+ * comparison against `NaN` is false, which produces an array `sort` calls sorted and is not,
93
+ * and then the de-duplication pass below compares against the wrong neighbour.
94
+ */
95
+ export declare function normalizeChapters(chapters: readonly VideoChapter[], duration: number): VideoChapter[];
96
+ /**
97
+ * Why this chapter cannot be placed, or `null` when it can.
98
+ *
99
+ * 🔴 A SENTENCE, not a boolean, and the same sentence in the editor and in whatever route ends
100
+ * up storing one. All four refusals are reachable in a few seconds of marking, and each names
101
+ * the thing to do about it rather than the rule it broke.
102
+ */
103
+ export declare function chapterProblem(draft: VideoChapter, existing: readonly VideoChapter[], duration: number): string | null;
104
+ /**
105
+ * The sections themselves: every chapter runs until the next one begins, and the last runs to
106
+ * the end of the clip.
107
+ *
108
+ * This is what both surfaces read — the editor's strip and the player's chapters track — from
109
+ * one function, because the alternative is two independent readings of "where does this
110
+ * section end" and a scrub bar whose dividers disagree with the list beside it.
111
+ *
112
+ * The clip before the FIRST chapter is deliberately not a span. A video whose first mark is at
113
+ * 2:00 has an unnamed opening, and inventing a section called nothing for it would put a row
114
+ * in the chapters menu that says less than no row at all.
115
+ */
116
+ export declare function chapterSpans(chapters: readonly VideoChapter[], duration: number): ChapterSpan[];
117
+ //# sourceMappingURL=videoChapters.d.ts.map