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.
- package/dist/react/components/ListToolbar.d.ts +94 -0
- package/dist/react/components/ListToolbar.d.ts.map +1 -0
- package/dist/react/components/ListToolbar.js +66 -0
- package/dist/react/components/ListToolbar.js.map +1 -0
- package/dist/react/file-tree/FileTree.d.ts +282 -3
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +799 -131
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/react/file-tree/fileTree.css +151 -0
- package/dist/react/file-tree/fileTreeSelection.d.ts +187 -0
- package/dist/react/file-tree/fileTreeSelection.d.ts.map +1 -0
- package/dist/react/file-tree/fileTreeSelection.js +264 -0
- package/dist/react/file-tree/fileTreeSelection.js.map +1 -0
- package/dist/react/file-tree/index.d.ts +1 -0
- package/dist/react/file-tree/index.d.ts.map +1 -1
- package/dist/react/file-tree/index.js +8 -0
- package/dist/react/file-tree/index.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/lib/form.d.ts +3 -0
- package/dist/react/lib/form.d.ts.map +1 -1
- package/dist/react/lib/form.js +43 -5
- package/dist/react/lib/form.js.map +1 -1
- package/dist/react/media/DenseGalleryShell.d.ts +19 -3
- package/dist/react/media/DenseGalleryShell.d.ts.map +1 -1
- package/dist/react/media/DenseGalleryShell.js +55 -15
- package/dist/react/media/DenseGalleryShell.js.map +1 -1
- package/dist/react/media/FocusViewer.d.ts.map +1 -1
- package/dist/react/media/FocusViewer.js +14 -14
- package/dist/react/media/FocusViewer.js.map +1 -1
- package/dist/react/media/FrameGrabber.d.ts +23 -0
- package/dist/react/media/FrameGrabber.d.ts.map +1 -1
- package/dist/react/media/FrameGrabber.js +67 -1
- package/dist/react/media/FrameGrabber.js.map +1 -1
- package/dist/react/media/MediaResize.d.ts +51 -19
- package/dist/react/media/MediaResize.d.ts.map +1 -1
- package/dist/react/media/MediaResize.js +20 -13
- package/dist/react/media/MediaResize.js.map +1 -1
- package/dist/react/media/VideoChapterEditor.d.ts +67 -0
- package/dist/react/media/VideoChapterEditor.d.ts.map +1 -0
- package/dist/react/media/VideoChapterEditor.js +148 -0
- package/dist/react/media/VideoChapterEditor.js.map +1 -0
- package/dist/react/media/VideoPlayer.d.ts +46 -4
- package/dist/react/media/VideoPlayer.d.ts.map +1 -1
- package/dist/react/media/VideoPlayer.js +124 -11
- package/dist/react/media/VideoPlayer.js.map +1 -1
- package/dist/react/media/VideoTurner.d.ts +15 -2
- package/dist/react/media/VideoTurner.d.ts.map +1 -1
- package/dist/react/media/VideoTurner.js +29 -2
- package/dist/react/media/VideoTurner.js.map +1 -1
- package/dist/react/media/denseGallery.css +104 -46
- package/dist/react/media/hlsSource.d.ts +57 -0
- package/dist/react/media/hlsSource.d.ts.map +1 -1
- package/dist/react/media/hlsSource.js +177 -1
- package/dist/react/media/hlsSource.js.map +1 -1
- package/dist/react/media/index.d.ts +2 -0
- package/dist/react/media/index.d.ts.map +1 -1
- package/dist/react/media/index.js +6 -0
- package/dist/react/media/index.js.map +1 -1
- package/dist/react/media/mediaResize.css +127 -54
- package/dist/react/media/pictureEditor.css +52 -6
- package/dist/react/media/videoChapterEditor.css +203 -0
- package/dist/react/media/videoChapters.d.ts +117 -0
- package/dist/react/media/videoChapters.d.ts.map +1 -0
- package/dist/react/media/videoChapters.js +95 -0
- package/dist/react/media/videoChapters.js.map +1 -0
- package/dist/react/media-gallery/GalleryTable.d.ts +52 -1
- package/dist/react/media-gallery/GalleryTable.d.ts.map +1 -1
- package/dist/react/media-gallery/GalleryTable.js +40 -9
- package/dist/react/media-gallery/GalleryTable.js.map +1 -1
- package/dist/react/media-gallery/MediaGallery.d.ts +107 -4
- package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
- package/dist/react/media-gallery/MediaGallery.js +484 -104
- package/dist/react/media-gallery/MediaGallery.js.map +1 -1
- package/dist/react/media-gallery/MediaMetaEditor.d.ts +48 -0
- package/dist/react/media-gallery/MediaMetaEditor.d.ts.map +1 -0
- package/dist/react/media-gallery/MediaMetaEditor.js +66 -0
- package/dist/react/media-gallery/MediaMetaEditor.js.map +1 -0
- package/dist/react/media-gallery/index.d.ts +2 -0
- package/dist/react/media-gallery/index.d.ts.map +1 -1
- package/dist/react/media-gallery/index.js +5 -0
- package/dist/react/media-gallery/index.js.map +1 -1
- package/dist/react/media-gallery/mediaGallery.css +180 -4
- package/dist/react/media-gallery/mediaMeta.d.ts +62 -0
- package/dist/react/media-gallery/mediaMeta.d.ts.map +1 -0
- package/dist/react/media-gallery/mediaMeta.js +56 -0
- package/dist/react/media-gallery/mediaMeta.js.map +1 -0
- package/dist/react/media-gallery/playbackPreferences.d.ts +77 -0
- package/dist/react/media-gallery/playbackPreferences.d.ts.map +1 -1
- package/dist/react/media-gallery/playbackPreferences.js +42 -0
- package/dist/react/media-gallery/playbackPreferences.js.map +1 -1
- package/dist/react/media-gallery/types.d.ts +34 -0
- package/dist/react/media-gallery/types.d.ts.map +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts +29 -0
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -0
- package/dist/styles-areas/activity-bar.css +30 -0
- package/dist/styles-areas/analytics.css +672 -0
- package/dist/styles-areas/auth.css +154 -0
- package/dist/styles-areas/calendar.css +416 -0
- package/dist/styles-areas/chart-table.css +41 -0
- package/dist/styles-areas/charts.css +89 -0
- package/dist/styles-areas/chat.css +364 -0
- package/dist/styles-areas/clipboard.css +26 -0
- package/dist/styles-areas/code-editor.css +130 -0
- package/dist/styles-areas/companion-link.css +70 -0
- package/dist/styles-areas/core.css +2503 -0
- package/dist/styles-areas/dashboard-grid.css +84 -0
- package/dist/styles-areas/data-table.css +657 -0
- package/dist/styles-areas/deep-link.css +26 -0
- package/dist/styles-areas/diff-viewer.css +30 -0
- package/dist/styles-areas/disk-usage.css +262 -0
- package/dist/styles-areas/emoji.css +154 -0
- package/dist/styles-areas/fields.css +582 -0
- package/dist/styles-areas/file-tree.css +33 -0
- package/dist/styles-areas/filter-rail.css +235 -0
- package/dist/styles-areas/folder-tree.css +219 -0
- package/dist/styles-areas/keep-awake.css +26 -0
- package/dist/styles-areas/layout-engine.css +239 -0
- package/dist/styles-areas/markdown.css +87 -0
- package/dist/styles-areas/master-detail.css +239 -0
- package/dist/styles-areas/master-lock.css +26 -0
- package/dist/styles-areas/media-gallery.css +351 -0
- package/dist/styles-areas/media.css +658 -0
- package/dist/styles-areas/nav.css +488 -0
- package/dist/styles-areas/notifications.css +245 -0
- package/dist/styles-areas/overlays.css +133 -0
- package/dist/styles-areas/palette.css +140 -0
- package/dist/styles-areas/pdf-viewer.css +47 -0
- package/dist/styles-areas/problem-detail.css +97 -0
- package/dist/styles-areas/rich-text.css +167 -0
- package/dist/styles-areas/sharing.css +153 -0
- package/dist/styles-areas/spreadsheet.css +37 -0
- package/dist/styles-areas/stats.css +281 -0
- package/dist/styles-areas/test-report.css +133 -0
- package/dist/styles-areas/virtual.css +30 -0
- package/dist/styles-areas/wizard.css +381 -0
- package/dist/styles-areas/workbench.css +543 -0
- package/dist/styles-areas/workbook-viewer.css +44 -0
- package/dist/styles-static.css +34 -23
- package/dist/styles.css +34 -23
- package/package.json +111 -31
- package/scripts/checkDistExports.ts +67 -0
- package/scripts/cssRules.ts +125 -0
- package/scripts/fixtureAppCss.ts +344 -0
- package/scripts/generateAreaStyles.ts +253 -0
- package/scripts/guardrailsEnforce.spec.ts +62 -0
- package/scripts/guardrailsEnforce.ts +67 -3
- package/scripts/styleAreas.ts +585 -0
- package/scripts/verify.ts +5 -0
- package/src/barrelsReachNoOptionalPeer.spec.ts +117 -9
- package/src/docsMatchTheSplit.spec.ts +98 -0
- package/src/fixtureAppCss.spec.ts +177 -0
- package/src/namedSubpathsResolve.spec.ts +35 -0
- package/src/publishShape.spec.ts +67 -0
- package/src/react/components/ListToolbar.spec.tsx +172 -0
- package/src/react/components/ListToolbar.tsx +151 -0
- package/src/react/file-tree/FileTree.spec.tsx +1009 -0
- package/src/react/file-tree/FileTree.tsx +1459 -309
- package/src/react/file-tree/fileTree.css +151 -0
- package/src/react/file-tree/fileTreeSelection.spec.ts +327 -0
- package/src/react/file-tree/fileTreeSelection.ts +321 -0
- package/src/react/file-tree/index.ts +21 -0
- package/src/react/index.ts +1 -0
- package/src/react/lib/form.ts +51 -8
- package/src/react/media/DenseGalleryShell.tsx +108 -36
- package/src/react/media/FocusViewer.tsx +15 -8
- package/src/react/media/FrameGrabber.spec.tsx +175 -0
- package/src/react/media/FrameGrabber.tsx +67 -0
- package/src/react/media/MediaResize.spec.tsx +84 -14
- package/src/react/media/MediaResize.tsx +51 -19
- package/src/react/media/VideoChapterEditor.tsx +428 -0
- package/src/react/media/VideoPlayer.spec.tsx +111 -1
- package/src/react/media/VideoPlayer.tsx +158 -11
- package/src/react/media/VideoTurner.spec.tsx +67 -0
- package/src/react/media/VideoTurner.tsx +70 -15
- package/src/react/media/denseGallery.css +104 -46
- package/src/react/media/hlsSource.recovery.spec.ts +213 -0
- package/src/react/media/hlsSource.spec.ts +8 -0
- package/src/react/media/hlsSource.ts +189 -1
- package/src/react/media/index.ts +14 -0
- package/src/react/media/mediaResize.css +127 -54
- package/src/react/media/pictureEditor.css +52 -6
- package/src/react/media/videoChapterEditor.css +203 -0
- package/src/react/media/videoChapters.spec.ts +176 -0
- package/src/react/media/videoChapters.ts +181 -0
- package/src/react/media-gallery/GalleryTable.tsx +93 -3
- package/src/react/media-gallery/MediaGallery.spec.tsx +1156 -10
- package/src/react/media-gallery/MediaGallery.tsx +813 -95
- package/src/react/media-gallery/MediaMetaEditor.tsx +237 -0
- package/src/react/media-gallery/galleryTable.spec.ts +41 -1
- package/src/react/media-gallery/index.ts +7 -0
- package/src/react/media-gallery/mediaGallery.css +180 -4
- package/src/react/media-gallery/mediaMeta.spec.ts +95 -0
- package/src/react/media-gallery/mediaMeta.ts +101 -0
- package/src/react/media-gallery/playbackPreferences.spec.ts +172 -0
- package/src/react/media-gallery/playbackPreferences.ts +100 -0
- package/src/react/media-gallery/types.ts +34 -0
- package/src/shippedFilesAreTracked.spec.ts +69 -0
- package/src/styles-areas/activity-bar.css +30 -0
- package/src/styles-areas/analytics.css +672 -0
- package/src/styles-areas/auth.css +154 -0
- package/src/styles-areas/calendar.css +416 -0
- package/src/styles-areas/chart-table.css +41 -0
- package/src/styles-areas/charts.css +89 -0
- package/src/styles-areas/chat.css +364 -0
- package/src/styles-areas/clipboard.css +26 -0
- package/src/styles-areas/code-editor.css +130 -0
- package/src/styles-areas/companion-link.css +70 -0
- package/src/styles-areas/core.css +2503 -0
- package/src/styles-areas/dashboard-grid.css +84 -0
- package/src/styles-areas/data-table.css +657 -0
- package/src/styles-areas/deep-link.css +26 -0
- package/src/styles-areas/diff-viewer.css +30 -0
- package/src/styles-areas/disk-usage.css +262 -0
- package/src/styles-areas/emoji.css +154 -0
- package/src/styles-areas/fields.css +582 -0
- package/src/styles-areas/file-tree.css +33 -0
- package/src/styles-areas/filter-rail.css +235 -0
- package/src/styles-areas/folder-tree.css +219 -0
- package/src/styles-areas/keep-awake.css +26 -0
- package/src/styles-areas/layout-engine.css +239 -0
- package/src/styles-areas/markdown.css +87 -0
- package/src/styles-areas/master-detail.css +239 -0
- package/src/styles-areas/master-lock.css +26 -0
- package/src/styles-areas/media-gallery.css +351 -0
- package/src/styles-areas/media.css +658 -0
- package/src/styles-areas/nav.css +488 -0
- package/src/styles-areas/notifications.css +245 -0
- package/src/styles-areas/overlays.css +133 -0
- package/src/styles-areas/palette.css +140 -0
- package/src/styles-areas/pdf-viewer.css +47 -0
- package/src/styles-areas/problem-detail.css +97 -0
- package/src/styles-areas/rich-text.css +167 -0
- package/src/styles-areas/sharing.css +153 -0
- package/src/styles-areas/spreadsheet.css +37 -0
- package/src/styles-areas/stats.css +281 -0
- package/src/styles-areas/test-report.css +133 -0
- package/src/styles-areas/virtual.css +30 -0
- package/src/styles-areas/wizard.css +381 -0
- package/src/styles-areas/workbench.css +543 -0
- package/src/styles-areas/workbook-viewer.css +44 -0
- package/src/styles-static.css +34 -23
- package/src/styles.css +34 -23
- package/src/stylesAreas.spec.ts +247 -0
- package/src/stylesUtilitiesMatches.spec.ts +6 -77
- package/src/testFilesRunInParallel.spec.ts +79 -0
- package/src/typecheckCachesAreSeparate.spec.ts +128 -0
- 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
|
|
48
|
+
* 🔴 ONE number: the grip's reach AND the height of the rail that holds it.
|
|
16
49
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Declared on `:root` because the
|
|
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
|
-
|
|
33
|
-
|
|
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`)
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
/*
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"*.
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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`
|
|
183
|
-
*
|
|
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
|
-
/*
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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:
|
|
273
|
-
max-inline-size:
|
|
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
|