@charcuterie/ui 3.32.0 → 3.34.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/README.md +30 -0
- package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts +16 -0
- package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts.map +1 -1
- package/dist/AdaptiveGrid/useAdaptiveColumns.js +1 -0
- package/dist/AdaptiveGrid/useAdaptiveColumns.js.map +1 -1
- package/dist/Board/Board.d.ts.map +1 -1
- package/dist/Board/Board.js +2 -2
- package/dist/Board/Board.js.map +1 -1
- package/dist/RangeSlider/RangeSlider.d.ts +117 -0
- package/dist/RangeSlider/RangeSlider.d.ts.map +1 -0
- package/dist/RangeSlider/RangeSlider.js +242 -0
- package/dist/RangeSlider/RangeSlider.js.map +1 -0
- package/dist/RangeSlider/rangeSliderValue.d.ts +102 -0
- package/dist/RangeSlider/rangeSliderValue.d.ts.map +1 -0
- package/dist/RangeSlider/rangeSliderValue.js +116 -0
- package/dist/RangeSlider/rangeSliderValue.js.map +1 -0
- package/dist/SegmentedControl/SegmentedControl.d.ts +12 -1
- package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControl.js +3 -3
- package/dist/SegmentedControl/SegmentedControl.js.map +1 -1
- package/dist/SegmentedControl/SegmentedOption.d.ts +2 -1
- package/dist/SegmentedControl/SegmentedOption.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedOption.js +7 -2
- package/dist/SegmentedControl/SegmentedOption.js.map +1 -1
- package/dist/Slider/Slider.d.ts +6 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +8 -22
- package/dist/Slider/Slider.js.map +1 -1
- package/dist/TimecodeInput/TimecodeInput.d.ts +88 -0
- package/dist/TimecodeInput/TimecodeInput.d.ts.map +1 -0
- package/dist/TimecodeInput/TimecodeInput.js +258 -0
- package/dist/TimecodeInput/TimecodeInput.js.map +1 -0
- package/dist/TimecodeInput/timecode.d.ts +132 -0
- package/dist/TimecodeInput/timecode.d.ts.map +1 -0
- package/dist/TimecodeInput/timecode.js +246 -0
- package/dist/TimecodeInput/timecode.js.map +1 -0
- package/dist/VirtualizedGrid/VirtualizedGrid.d.ts +35 -0
- package/dist/VirtualizedGrid/VirtualizedGrid.d.ts.map +1 -1
- package/dist/VirtualizedGrid/VirtualizedGrid.js +37 -1
- package/dist/VirtualizedGrid/VirtualizedGrid.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/sliderStyles.d.ts +39 -0
- package/dist/sliderStyles.d.ts.map +1 -0
- package/dist/sliderStyles.js +50 -0
- package/dist/sliderStyles.js.map +1 -0
- package/package.json +1 -1
- package/src/AdaptiveGrid/useAdaptiveColumns.ts +17 -0
- package/src/Board/Board.test.tsx +8 -0
- package/src/Board/Board.tsx +7 -1
- package/src/RangeSlider/RangeSlider.mdx +85 -0
- package/src/RangeSlider/RangeSlider.stories.tsx +346 -0
- package/src/RangeSlider/RangeSlider.test.tsx +452 -0
- package/src/RangeSlider/RangeSlider.tsx +595 -0
- package/src/RangeSlider/rangeSliderValue.test.ts +217 -0
- package/src/RangeSlider/rangeSliderValue.ts +185 -0
- package/src/SegmentedControl/SegmentedControl.stories.tsx +50 -0
- package/src/SegmentedControl/SegmentedControl.test.tsx +36 -1
- package/src/SegmentedControl/SegmentedControl.tsx +14 -0
- package/src/SegmentedControl/SegmentedOption.tsx +7 -0
- package/src/Slider/Slider.tsx +23 -29
- package/src/TimecodeInput/TimecodeInput.mdx +155 -0
- package/src/TimecodeInput/TimecodeInput.stories.tsx +304 -0
- package/src/TimecodeInput/TimecodeInput.test.tsx +279 -0
- package/src/TimecodeInput/TimecodeInput.tsx +493 -0
- package/src/TimecodeInput/timecode.test.ts +236 -0
- package/src/TimecodeInput/timecode.ts +391 -0
- package/src/VirtualizedGrid/VirtualizedGrid.mdx +21 -0
- package/src/VirtualizedGrid/VirtualizedGrid.stories.tsx +64 -0
- package/src/VirtualizedGrid/VirtualizedGrid.test.tsx +122 -3
- package/src/VirtualizedGrid/VirtualizedGrid.tsx +37 -1
- package/src/index.ts +18 -0
- package/src/sliderStyles.ts +69 -0
- package/src/sourceRules.test.ts +25 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createElement as _createElement } from "react";
|
|
3
|
+
import { useUniqueId } from "@charcuterie/logic";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { CONTROL_SIZE_CLASS } from "../controlStyles.js";
|
|
6
|
+
import { DISABLED_CLASS, FOCUS_RING_CLASS, } from "../intentStyles.js";
|
|
7
|
+
import { mergeSlotProps } from "../slotProps.js";
|
|
8
|
+
import { toClassName } from "../toClassName.js";
|
|
9
|
+
import { clampTimecode, formatTimecode, parseTimecodeInput, } from "./timecode.js";
|
|
10
|
+
const INPUT_CLASS = "w-full min-w-0 rounded-md border border-border-default bg-surface-raised text-content-primary transition-colors duration-(--duration-fast) ease-standard placeholder:text-content-muted hover:border-border-strong aria-invalid:border-intent-danger-border";
|
|
11
|
+
const toSeededRange = (valueMs) => valueMs === undefined
|
|
12
|
+
? { end: null, start: null }
|
|
13
|
+
: typeof valueMs === "number"
|
|
14
|
+
? { end: null, start: valueMs }
|
|
15
|
+
: valueMs;
|
|
16
|
+
/**
|
|
17
|
+
* A timecode field: a text input a person can actually type a media
|
|
18
|
+
* position into, with an `isRange` mode for a section of a file.
|
|
19
|
+
*
|
|
20
|
+
* ### Why this is a text field and not `<input type="time">`
|
|
21
|
+
*
|
|
22
|
+
* Two reasons and neither is taste.
|
|
23
|
+
*
|
|
24
|
+
* A `time` input's smallest step is a **second** unless `step` is
|
|
25
|
+
* fractional, its value is a wall-clock `HH:MM` string capped at 24
|
|
26
|
+
* hours, and it has no way to express "1.5 hours into a file" as a
|
|
27
|
+
* duration rather than as half past one. A clip boundary in a video
|
|
28
|
+
* editor is milliseconds; the platform control cannot carry them.
|
|
29
|
+
*
|
|
30
|
+
* And it is painted by the operating system, from
|
|
31
|
+
* `::-webkit-datetime-edit-*` pseudo-elements no token reaches
|
|
32
|
+
* inside — the same mechanism that
|
|
33
|
+
* [deprecated the native `Select`](../../../../docs/decisions/2026-08-20-native-select-is-deprecated-and-the-platform-hatch-is-closed.md)
|
|
34
|
+
* and that kept `Slider` off
|
|
35
|
+
* [`<input type="range">`](../../../../docs/decisions/2026-08-22-the-slider-is-a-div-with-role-slider-not-an-input-range.md).
|
|
36
|
+
* A `<input type="text">` this component owns every pixel of is the
|
|
37
|
+
* settled answer to that shape of question.
|
|
38
|
+
*
|
|
39
|
+
* ### Typing is the whole interface, so nothing guesses in silence
|
|
40
|
+
*
|
|
41
|
+
* `90`, `1:30`, `1:02:03` and `1:02:03.500` all resolve, and the
|
|
42
|
+
* resolution is echoed underneath the field, in a live region, before
|
|
43
|
+
* anything commits. Commit is Enter or blur; a keystroke never
|
|
44
|
+
* commits, a refused value leaves the text exactly as typed, and
|
|
45
|
+
* Escape puts back the last committed value. `timecode.ts` is the
|
|
46
|
+
* grammar and the reasoning.
|
|
47
|
+
*
|
|
48
|
+
* ### Both ends of a section are independently optional
|
|
49
|
+
*
|
|
50
|
+
* `{ end, start }` has four real states, not three plus an error: no
|
|
51
|
+
* window, from here to the end of the media, from the beginning up to
|
|
52
|
+
* here, and the window between two points. So there is no
|
|
53
|
+
* `isEndOpen` prop — `null` says it — and clearing one end never
|
|
54
|
+
* touches the other.
|
|
55
|
+
*
|
|
56
|
+
* An **inverted** section swaps, because throwing the pair away would
|
|
57
|
+
* discard the boundary just typed. A **zero-length** section is
|
|
58
|
+
* refused by name: it plays nothing, so swapping would hide a typo
|
|
59
|
+
* behind a window that silently does not exist.
|
|
60
|
+
*/
|
|
61
|
+
export const TimecodeInput = ({ className, durationMs, isDisabled = false, isRange = false, label, maxValueMs, minValueMs, onChange, placeholder = "e.g. 1:02:03.500", size = "md", stepMs = 1_000, valueMs, ...receivedSlotProps }) => {
|
|
62
|
+
const baseId = useUniqueId();
|
|
63
|
+
const minimum = minValueMs ?? 0;
|
|
64
|
+
const maximum = maxValueMs ?? durationMs;
|
|
65
|
+
const [seededRange] = useState(() => toSeededRange(valueMs));
|
|
66
|
+
const [range, setRange] = useState(seededRange);
|
|
67
|
+
const toDisplayText = (milliseconds) => milliseconds === null
|
|
68
|
+
? ""
|
|
69
|
+
: formatTimecode(milliseconds);
|
|
70
|
+
const [texts, setTexts] = useState({
|
|
71
|
+
end: toDisplayText(seededRange.end),
|
|
72
|
+
start: toDisplayText(seededRange.start),
|
|
73
|
+
});
|
|
74
|
+
const [activeEndpoint, setActiveEndpoint] = useState("start");
|
|
75
|
+
/**
|
|
76
|
+
* A refusal the grammar cannot see, because it is about the
|
|
77
|
+
* **pair** rather than about either text: a zero-length section.
|
|
78
|
+
* Cleared by the next keystroke, so an edit always gets a fresh
|
|
79
|
+
* reading rather than an old complaint.
|
|
80
|
+
*/
|
|
81
|
+
const [rangeRefusal, setRangeRefusal] = useState(null);
|
|
82
|
+
const parsed = parseTimecodeInput(texts[activeEndpoint]);
|
|
83
|
+
const echoId = `${baseId}-echo`;
|
|
84
|
+
const report = (nextRange) => {
|
|
85
|
+
setRange(nextRange);
|
|
86
|
+
if (isRange) {
|
|
87
|
+
onChange?.(nextRange.end === null && nextRange.start === null
|
|
88
|
+
? null
|
|
89
|
+
: nextRange);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
onChange?.(nextRange.start);
|
|
93
|
+
};
|
|
94
|
+
const writeEndpoint = (endpoint, milliseconds) => {
|
|
95
|
+
if (!isRange) {
|
|
96
|
+
setTexts({
|
|
97
|
+
end: "",
|
|
98
|
+
start: toDisplayText(milliseconds),
|
|
99
|
+
});
|
|
100
|
+
setRangeRefusal(null);
|
|
101
|
+
report({ end: null, start: milliseconds });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const nextRange = {
|
|
105
|
+
...range,
|
|
106
|
+
[endpoint]: milliseconds,
|
|
107
|
+
};
|
|
108
|
+
// Only a pair can be compared. An open start is the absence of a
|
|
109
|
+
// choice rather than zero, so a section that only says where to
|
|
110
|
+
// stop is clamped and accepted — and stays able to complain the
|
|
111
|
+
// moment a start arrives that really does sit after it.
|
|
112
|
+
const isPair = nextRange.end !== null && nextRange.start !== null;
|
|
113
|
+
if (isPair && nextRange.end === nextRange.start) {
|
|
114
|
+
setRangeRefusal(`A section needs a length, and this one starts and ends at ${toDisplayText(milliseconds)}.`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const isBackwards = isPair &&
|
|
118
|
+
(nextRange.end ?? 0) < (nextRange.start ?? 0);
|
|
119
|
+
const settled = isBackwards
|
|
120
|
+
? { end: nextRange.start, start: nextRange.end }
|
|
121
|
+
: nextRange;
|
|
122
|
+
setTexts(isBackwards
|
|
123
|
+
? {
|
|
124
|
+
end: toDisplayText(settled.end),
|
|
125
|
+
start: toDisplayText(settled.start),
|
|
126
|
+
}
|
|
127
|
+
: (previous) => ({
|
|
128
|
+
...previous,
|
|
129
|
+
[endpoint]: toDisplayText(milliseconds),
|
|
130
|
+
}));
|
|
131
|
+
setRangeRefusal(null);
|
|
132
|
+
report(settled);
|
|
133
|
+
};
|
|
134
|
+
const commitText = (endpoint) => {
|
|
135
|
+
const result = parseTimecodeInput(texts[endpoint]);
|
|
136
|
+
if (result.kind === "empty") {
|
|
137
|
+
writeEndpoint(endpoint, null);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (result.kind === "unparsed") {
|
|
141
|
+
// The text stays exactly as typed. Clearing it throws away
|
|
142
|
+
// what the person has to edit; rewriting it to the last good
|
|
143
|
+
// value discards what they meant.
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
writeEndpoint(endpoint, clampTimecode(result.milliseconds, minimum, maximum));
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* ArrowUp and ArrowDown, which **do** commit.
|
|
150
|
+
*
|
|
151
|
+
* That is not an exception to "a keystroke never commits" — it is
|
|
152
|
+
* the other side of it. Typing is a draft the echo reports on;
|
|
153
|
+
* a step is a whole gesture on a value that already exists, the
|
|
154
|
+
* same act as clicking a day in `DatePicker`'s grid, and a
|
|
155
|
+
* stepper that needed Enter afterwards would be a stepper nobody
|
|
156
|
+
* pressed twice.
|
|
157
|
+
*/
|
|
158
|
+
const stepEndpoint = (endpoint, direction, isCoarse) => {
|
|
159
|
+
const result = parseTimecodeInput(texts[endpoint]);
|
|
160
|
+
const base = result.kind === "timecode"
|
|
161
|
+
? result.milliseconds
|
|
162
|
+
: (range[endpoint] ?? 0);
|
|
163
|
+
writeEndpoint(endpoint, clampTimecode(base + stepMs * (isCoarse ? 10 : 1) * direction, minimum, maximum));
|
|
164
|
+
};
|
|
165
|
+
const handleKeyDown = (keyEvent, endpoint) => {
|
|
166
|
+
if (keyEvent.key === "Enter") {
|
|
167
|
+
keyEvent.preventDefault();
|
|
168
|
+
commitText(endpoint);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (keyEvent.key === "Escape") {
|
|
172
|
+
keyEvent.preventDefault();
|
|
173
|
+
setRangeRefusal(null);
|
|
174
|
+
setTexts((previous) => ({
|
|
175
|
+
...previous,
|
|
176
|
+
[endpoint]: toDisplayText(range[endpoint]),
|
|
177
|
+
}));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (keyEvent.key === "ArrowUp" ||
|
|
181
|
+
keyEvent.key === "ArrowDown") {
|
|
182
|
+
keyEvent.preventDefault();
|
|
183
|
+
stepEndpoint(endpoint, keyEvent.key === "ArrowUp" ? 1 : -1, keyEvent.shiftKey);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const clampedPreview = parsed.kind === "timecode"
|
|
187
|
+
? clampTimecode(parsed.milliseconds, minimum, maximum)
|
|
188
|
+
: null;
|
|
189
|
+
const parsedEcho = parsed.kind === "unparsed"
|
|
190
|
+
? parsed.reason
|
|
191
|
+
: parsed.kind === "empty" || clampedPreview === null
|
|
192
|
+
? ""
|
|
193
|
+
: clampedPreview === parsed.milliseconds
|
|
194
|
+
? formatTimecode(parsed.milliseconds)
|
|
195
|
+
: `${formatTimecode(parsed.milliseconds)} is outside the media, so it commits as ${formatTimecode(clampedPreview)}.`;
|
|
196
|
+
const echoText = rangeRefusal ?? parsedEcho;
|
|
197
|
+
const isInvalid = rangeRefusal !== null || parsed.kind === "unparsed";
|
|
198
|
+
const renderInput = (endpoint) => {
|
|
199
|
+
const ownProps = {
|
|
200
|
+
"aria-describedby": echoId,
|
|
201
|
+
"aria-invalid": activeEndpoint === endpoint && isInvalid
|
|
202
|
+
? true
|
|
203
|
+
: undefined,
|
|
204
|
+
"aria-label": isRange
|
|
205
|
+
? `${label} ${endpoint}`
|
|
206
|
+
: label,
|
|
207
|
+
autoComplete: "off",
|
|
208
|
+
className: toClassName(INPUT_CLASS, CONTROL_SIZE_CLASS[size], FOCUS_RING_CLASS, DISABLED_CLASS),
|
|
209
|
+
disabled: isDisabled,
|
|
210
|
+
// A `Field` above mints an `id` for its `<label htmlFor>` and
|
|
211
|
+
// clones it down. Preferring it over the generated one is what
|
|
212
|
+
// keeps that label pointing at something — the generated id is
|
|
213
|
+
// referenced by nothing here, so there is nothing to lose by
|
|
214
|
+
// standing aside for a caller's.
|
|
215
|
+
id: endpoint === "start"
|
|
216
|
+
? (receivedSlotProps.id ?? `${baseId}-start`)
|
|
217
|
+
: `${baseId}-end`,
|
|
218
|
+
// A timecode is digits and two separators, so the numeric
|
|
219
|
+
// keypad is the right soft keyboard — and a bare seconds count
|
|
220
|
+
// is a first-class form of the grammar precisely because iOS
|
|
221
|
+
// gives that keypad no colon. `90` and `5400` are complete
|
|
222
|
+
// timecodes, so the phone case is answered by the grammar
|
|
223
|
+
// rather than by a keyboard the field cannot choose.
|
|
224
|
+
inputMode: "numeric",
|
|
225
|
+
onBlur: () => {
|
|
226
|
+
commitText(endpoint);
|
|
227
|
+
},
|
|
228
|
+
onChange: (changeEvent) => {
|
|
229
|
+
// Read out of the event NOW. React nulls `currentTarget`
|
|
230
|
+
// once the handler returns, and a `setTexts` updater runs
|
|
231
|
+
// after that — so reaching for it inside the updater is a
|
|
232
|
+
// `Cannot read properties of null` at the next render.
|
|
233
|
+
const nextText = changeEvent.currentTarget.value;
|
|
234
|
+
setRangeRefusal(null);
|
|
235
|
+
setTexts((previous) => ({
|
|
236
|
+
...previous,
|
|
237
|
+
[endpoint]: nextText,
|
|
238
|
+
}));
|
|
239
|
+
},
|
|
240
|
+
onFocus: () => {
|
|
241
|
+
setActiveEndpoint(endpoint);
|
|
242
|
+
},
|
|
243
|
+
onKeyDown: (keyEvent) => {
|
|
244
|
+
handleKeyDown(keyEvent, endpoint);
|
|
245
|
+
},
|
|
246
|
+
placeholder,
|
|
247
|
+
type: "text",
|
|
248
|
+
value: texts[endpoint],
|
|
249
|
+
};
|
|
250
|
+
return (_createElement("input", { ...(endpoint === "start"
|
|
251
|
+
? mergeSlotProps(receivedSlotProps, ownProps)
|
|
252
|
+
: ownProps), key: endpoint }));
|
|
253
|
+
};
|
|
254
|
+
return (_jsxs("div", { className: toClassName("flex flex-col gap-1", className), children: [_jsxs("div", { className: "flex items-center gap-2", children: [renderInput("start"), isRange ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "shrink-0 text-content-secondary text-sm", children: "to" }), renderInput("end")] })) : null] }), _jsx("p", { "aria-live": "polite", className: toClassName("min-h-5 text-sm", isInvalid
|
|
255
|
+
? "text-intent-danger-content"
|
|
256
|
+
: "text-content-secondary"), id: echoId, role: "status", children: echoText })] }));
|
|
257
|
+
};
|
|
258
|
+
//# sourceMappingURL=TimecodeInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimecodeInput.js","sourceRoot":"","sources":["../../src/TimecodeInput/TimecodeInput.tsx"],"names":[],"mappings":";;AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAMhD,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EACL,cAAc,EACd,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,OAAO,EACL,aAAa,EACb,cAAc,EACd,kBAAkB,GACnB,MAAM,eAAe,CAAA;AAwCtB,MAAM,WAAW,GACf,6PAA6P,CAAA;AAI/P,MAAM,aAAa,GAAG,CACpB,OAA2C,EAC5B,EAAE,CACjB,OAAO,KAAK,SAAS;IACnB,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC5B,CAAC,CAAC,OAAO,OAAO,KAAK,QAAQ;QAC3B,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE;QAC/B,CAAC,CAAC,OAAO,CAAA;AAEf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAC5B,SAAS,EACT,UAAU,EACV,UAAU,GAAG,KAAK,EAClB,OAAO,GAAG,KAAK,EACf,KAAK,EACL,UAAU,EACV,UAAU,EACV,QAAQ,EACR,WAAW,GAAG,kBAAkB,EAChC,IAAI,GAAG,IAAI,EACX,MAAM,GAAG,KAAK,EACd,OAAO,EACP,GAAG,iBAAiB,EACD,EAAa,EAAE;IAClC,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA;IAE5B,MAAM,OAAO,GAAG,UAAU,IAAI,CAAC,CAAA;IAE/B,MAAM,OAAO,GAAG,UAAU,IAAI,UAAU,CAAA;IAExC,MAAM,CAAC,WAAW,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAClC,aAAa,CAAC,OAAO,CAAC,CACvB,CAAA;IAED,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GACrB,QAAQ,CAAgB,WAAW,CAAC,CAAA;IAEtC,MAAM,aAAa,GAAG,CAAC,YAA2B,EAAE,EAAE,CACpD,YAAY,KAAK,IAAI;QACnB,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,CAAA;IAElC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC;QACjC,GAAG,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,CAAC;QACnC,KAAK,EAAE,aAAa,CAAC,WAAW,CAAC,KAAK,CAAC;KACxC,CAAC,CAAA;IAEF,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GACvC,QAAQ,CAAW,OAAO,CAAC,CAAA;IAE7B;;;;;OAKG;IACH,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAE9C,IAAI,CAAC,CAAA;IAEP,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAA;IAExD,MAAM,MAAM,GAAG,GAAG,MAAM,OAAO,CAAA;IAE/B,MAAM,MAAM,GAAG,CAAC,SAAwB,EAAE,EAAE;QAC1C,QAAQ,CAAC,SAAS,CAAC,CAAA;QAEnB,IAAI,OAAO,EAAE,CAAC;YACZ,QAAQ,EAAE,CACR,SAAS,CAAC,GAAG,KAAK,IAAI,IAAI,SAAS,CAAC,KAAK,KAAK,IAAI;gBAChD,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,SAAS,CACd,CAAA;YAED,OAAM;QACR,CAAC;QAED,QAAQ,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAC7B,CAAC,CAAA;IAED,MAAM,aAAa,GAAG,CACpB,QAAkB,EAClB,YAA2B,EAC3B,EAAE;QACF,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,QAAQ,CAAC;gBACP,GAAG,EAAE,EAAE;gBACP,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC;aACnC,CAAC,CAAA;YAEF,eAAe,CAAC,IAAI,CAAC,CAAA;YAErB,MAAM,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAA;YAE1C,OAAM;QACR,CAAC;QAED,MAAM,SAAS,GAAG;YAChB,GAAG,KAAK;YACR,CAAC,QAAQ,CAAC,EAAE,YAAY;SACR,CAAA;QAElB,iEAAiE;QACjE,gEAAgE;QAChE,gEAAgE;QAChE,wDAAwD;QACxD,MAAM,MAAM,GACV,SAAS,CAAC,GAAG,KAAK,IAAI,IAAI,SAAS,CAAC,KAAK,KAAK,IAAI,CAAA;QAEpD,IAAI,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,SAAS,CAAC,KAAK,EAAE,CAAC;YAChD,eAAe,CACb,6DAA6D,aAAa,CAAC,YAAY,CAAC,GAAG,CAC5F,CAAA;YAED,OAAM;QACR,CAAC;QAED,MAAM,WAAW,GACf,MAAM;YACN,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,CAAC,CAAA;QAE/C,MAAM,OAAO,GAAG,WAAW;YACzB,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,EAAE;YAChD,CAAC,CAAC,SAAS,CAAA;QAEb,QAAQ,CACN,WAAW;YACT,CAAC,CAAC;gBACE,GAAG,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC;gBAC/B,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,CAAC;aACpC;YACH,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;gBACb,GAAG,QAAQ;gBACX,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC;aACxC,CAAC,CACP,CAAA;QAED,eAAe,CAAC,IAAI,CAAC,CAAA;QAErB,MAAM,CAAC,OAAO,CAAC,CAAA;IACjB,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,CAAC,QAAkB,EAAE,EAAE;QACxC,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAA;QAElD,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC5B,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;YAE7B,OAAM;QACR,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC/B,2DAA2D;YAC3D,6DAA6D;YAC7D,kCAAkC;YAClC,OAAM;QACR,CAAC;QAED,aAAa,CACX,QAAQ,EACR,aAAa,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,CACrD,CAAA;IACH,CAAC,CAAA;IAED;;;;;;;;;OASG;IACH,MAAM,YAAY,GAAG,CACnB,QAAkB,EAClB,SAAiB,EACjB,QAAiB,EACjB,EAAE;QACF,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAA;QAElD,MAAM,IAAI,GACR,MAAM,CAAC,IAAI,KAAK,UAAU;YACxB,CAAC,CAAC,MAAM,CAAC,YAAY;YACrB,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;QAE5B,aAAa,CACX,QAAQ,EACR,aAAa,CACX,IAAI,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,EAC/C,OAAO,EACP,OAAO,CACR,CACF,CAAA;IACH,CAAC,CAAA;IAED,MAAM,aAAa,GAAG,CACpB,QAA8C,EAC9C,QAAkB,EAClB,EAAE;QACF,IAAI,QAAQ,CAAC,GAAG,KAAK,OAAO,EAAE,CAAC;YAC7B,QAAQ,CAAC,cAAc,EAAE,CAAA;YAEzB,UAAU,CAAC,QAAQ,CAAC,CAAA;YAEpB,OAAM;QACR,CAAC;QAED,IAAI,QAAQ,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;YAC9B,QAAQ,CAAC,cAAc,EAAE,CAAA;YAEzB,eAAe,CAAC,IAAI,CAAC,CAAA;YAErB,QAAQ,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;gBACtB,GAAG,QAAQ;gBACX,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;aAC3C,CAAC,CAAC,CAAA;YAEH,OAAM;QACR,CAAC;QAED,IACE,QAAQ,CAAC,GAAG,KAAK,SAAS;YAC1B,QAAQ,CAAC,GAAG,KAAK,WAAW,EAC5B,CAAC;YACD,QAAQ,CAAC,cAAc,EAAE,CAAA;YAEzB,YAAY,CACV,QAAQ,EACR,QAAQ,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EACnC,QAAQ,CAAC,QAAQ,CAClB,CAAA;QACH,CAAC;IACH,CAAC,CAAA;IAED,MAAM,cAAc,GAClB,MAAM,CAAC,IAAI,KAAK,UAAU;QACxB,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC;QACtD,CAAC,CAAC,IAAI,CAAA;IAEV,MAAM,UAAU,GACd,MAAM,CAAC,IAAI,KAAK,UAAU;QACxB,CAAC,CAAC,MAAM,CAAC,MAAM;QACf,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,OAAO,IAAI,cAAc,KAAK,IAAI;YAClD,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,cAAc,KAAK,MAAM,CAAC,YAAY;gBACtC,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,YAAY,CAAC;gBACrC,CAAC,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC,YAAY,CAAC,2CAA2C,cAAc,CAAC,cAAc,CAAC,GAAG,CAAA;IAE5H,MAAM,QAAQ,GAAG,YAAY,IAAI,UAAU,CAAA;IAE3C,MAAM,SAAS,GACb,YAAY,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,CAAA;IAErD,MAAM,WAAW,GAAG,CAAC,QAAkB,EAAE,EAAE;QACzC,MAAM,QAAQ,GAAG;YACf,kBAAkB,EAAE,MAAM;YAC1B,cAAc,EACZ,cAAc,KAAK,QAAQ,IAAI,SAAS;gBACtC,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,SAAS;YACf,YAAY,EAAE,OAAO;gBACnB,CAAC,CAAC,GAAG,KAAK,IAAI,QAAQ,EAAE;gBACxB,CAAC,CAAC,KAAK;YACT,YAAY,EAAE,KAAc;YAC5B,SAAS,EAAE,WAAW,CACpB,WAAW,EACX,kBAAkB,CAAC,IAAI,CAAC,EACxB,gBAAgB,EAChB,cAAc,CACf;YACD,QAAQ,EAAE,UAAU;YACpB,8DAA8D;YAC9D,+DAA+D;YAC/D,+DAA+D;YAC/D,6DAA6D;YAC7D,iCAAiC;YACjC,EAAE,EACA,QAAQ,KAAK,OAAO;gBAClB,CAAC,CAAC,CAAC,iBAAiB,CAAC,EAAE,IAAI,GAAG,MAAM,QAAQ,CAAC;gBAC7C,CAAC,CAAC,GAAG,MAAM,MAAM;YACrB,0DAA0D;YAC1D,+DAA+D;YAC/D,6DAA6D;YAC7D,2DAA2D;YAC3D,0DAA0D;YAC1D,qDAAqD;YACrD,SAAS,EAAE,SAAkB;YAC7B,MAAM,EAAE,GAAG,EAAE;gBACX,UAAU,CAAC,QAAQ,CAAC,CAAA;YACtB,CAAC;YACD,QAAQ,EAAE,CAAC,WAEV,EAAE,EAAE;gBACH,yDAAyD;gBACzD,0DAA0D;gBAC1D,0DAA0D;gBAC1D,uDAAuD;gBACvD,MAAM,QAAQ,GAAG,WAAW,CAAC,aAAa,CAAC,KAAK,CAAA;gBAEhD,eAAe,CAAC,IAAI,CAAC,CAAA;gBAErB,QAAQ,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;oBACtB,GAAG,QAAQ;oBACX,CAAC,QAAQ,CAAC,EAAE,QAAQ;iBACrB,CAAC,CAAC,CAAA;YACL,CAAC;YACD,OAAO,EAAE,GAAG,EAAE;gBACZ,iBAAiB,CAAC,QAAQ,CAAC,CAAA;YAC7B,CAAC;YACD,SAAS,EAAE,CACT,QAA8C,EAC9C,EAAE;gBACF,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;YACnC,CAAC;YACD,WAAW;YACX,IAAI,EAAE,MAAe;YACrB,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC;SACvB,CAAA;QAED,OAAO,CACL,6BAKM,CAAC,QAAQ,KAAK,OAAO;gBACvB,CAAC,CAAC,cAAc,CAAC,iBAAiB,EAAE,QAAQ,CAAC;gBAC7C,CAAC,CAAC,QAAQ,CAAC,EACb,GAAG,EAAE,QAAQ,GACb,CACH,CAAA;IACH,CAAC,CAAA;IAED,OAAO,CACL,eACE,SAAS,EAAE,WAAW,CACpB,qBAAqB,EACrB,SAAS,CACV,aAED,eAAK,SAAS,EAAC,yBAAyB,aACrC,WAAW,CAAC,OAAO,CAAC,EAEpB,OAAO,CAAC,CAAC,CAAC,CACT,8BACE,8BACc,MAAM,EAClB,SAAS,EAAC,yCAAyC,mBAG9C,EAEN,WAAW,CAAC,KAAK,CAAC,IAClB,CACJ,CAAC,CAAC,CAAC,IAAI,IACJ,EAUN,yBACY,QAAQ,EAClB,SAAS,EAAE,WAAW,CACpB,iBAAiB,EACjB,SAAS;oBACP,CAAC,CAAC,4BAA4B;oBAC9B,CAAC,CAAC,wBAAwB,CAC7B,EACD,EAAE,EAAE,MAAM,EACV,IAAI,EAAC,QAAQ,YAEZ,QAAQ,GACP,IACA,CACP,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a human types into a timecode field, turned into a position
|
|
3
|
+
* in a piece of media — or into a stated reason it was not one.
|
|
4
|
+
*
|
|
5
|
+
* ### Milliseconds, and only milliseconds
|
|
6
|
+
*
|
|
7
|
+
* A media position is a **duration from the start of the file**, so
|
|
8
|
+
* the value is an integer count of milliseconds and nothing else. It
|
|
9
|
+
* is not a `Date`, it is not a clock time, and it is not a string:
|
|
10
|
+
* the only arithmetic anybody does with it is subtraction, and a
|
|
11
|
+
* number is the type that supports it. The props that carry one keep
|
|
12
|
+
* the `Ms` suffix, because a duration's unit is genuinely ambiguous
|
|
13
|
+
* and the fleet has both seconds and milliseconds in use
|
|
14
|
+
* ([decision](../../../../docs/decisions/2026-08-25-an-outbound-http-cache-names-its-fields-fetchedat-and-expiresat.md)).
|
|
15
|
+
*
|
|
16
|
+
* ### The fleet writes the printing half five times and the reading
|
|
17
|
+
* half never
|
|
18
|
+
*
|
|
19
|
+
* `formatTimecode` replaces five hand-rolled `h:mm:ss` printers —
|
|
20
|
+
* queuepilot's `clock` and `toClock`, castkit's `formatTime`, and
|
|
21
|
+
* mux-magic's two modal formatters — each with its own answer to
|
|
22
|
+
* whether the hour is shown and whether the minute is padded.
|
|
23
|
+
* `parseTimecodeInput` has no counterpart at all: nothing in the
|
|
24
|
+
* fleet *reads* a timecode a person typed, which is why the grammar
|
|
25
|
+
* gets the care here rather than being rediscovered per app.
|
|
26
|
+
*
|
|
27
|
+
* ### The rule that governs the grammar: never guess in silence
|
|
28
|
+
*
|
|
29
|
+
* The same three mechanisms `parseDateInput.ts` states, for the same
|
|
30
|
+
* reason:
|
|
31
|
+
*
|
|
32
|
+
* 1. **The grammar is ordered and total.** Each accepting rule is
|
|
33
|
+
* anchored on the *whole* input, the first match wins, and if
|
|
34
|
+
* none matches the answer is a named failure. `1:` is not one
|
|
35
|
+
* minute, and `1:90` is not `2:30`.
|
|
36
|
+
* 2. **An overflow is a failure with a name, not a carry.** `1:90`
|
|
37
|
+
* is refused, and the message offers both legal spellings —
|
|
38
|
+
* `2:30` and `90` — without committing to either. Silently
|
|
39
|
+
* carrying 90 seconds into the minute is the `1:60` trap
|
|
40
|
+
* mux-magic wrote down in
|
|
41
|
+
* `docs/audits/2026-06-29-pre-rename-domain-decisions.md`.
|
|
42
|
+
* 3. **Nothing commits without an echo.** `TimecodeInput` renders
|
|
43
|
+
* the resolved position in full underneath the field, in a live
|
|
44
|
+
* region, before anything commits.
|
|
45
|
+
*
|
|
46
|
+
* ### The largest field present is unbounded; every smaller one is
|
|
47
|
+
* 0–59
|
|
48
|
+
*
|
|
49
|
+
* One sentence covers every bound in the grammar. `90` is ninety
|
|
50
|
+
* **seconds**, `90:00` is ninety **minutes**, and `1:90` is refused
|
|
51
|
+
* because the minute beside it makes 90 a seconds field. That is why
|
|
52
|
+
* a bare number is seconds rather than minutes: seconds is the
|
|
53
|
+
* smallest unit the field prints, so the reading that needs no
|
|
54
|
+
* explanation is the one where the typist appended nothing.
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* A clip window, in milliseconds. Keys alphabetised, matching
|
|
58
|
+
* `DateRange`.
|
|
59
|
+
*
|
|
60
|
+
* **Both ends are independently optional**, and the four
|
|
61
|
+
* combinations are four real states rather than three plus an error:
|
|
62
|
+
* no window at all, from here to the end of the media, from the
|
|
63
|
+
* beginning up to here, and the window between two points. An open
|
|
64
|
+
* start is not zero — it is the absence of a choice — which is what
|
|
65
|
+
* lets a half-open window be clamped without being compared.
|
|
66
|
+
*/
|
|
67
|
+
export type TimecodeRange = {
|
|
68
|
+
end: null | number;
|
|
69
|
+
start: null | number;
|
|
70
|
+
};
|
|
71
|
+
export type TimecodeInputResult = {
|
|
72
|
+
kind: "empty";
|
|
73
|
+
} | {
|
|
74
|
+
kind: "timecode";
|
|
75
|
+
milliseconds: number;
|
|
76
|
+
} | {
|
|
77
|
+
kind: "unparsed";
|
|
78
|
+
reason: string;
|
|
79
|
+
};
|
|
80
|
+
export type FormatTimecodeOptions = {
|
|
81
|
+
/**
|
|
82
|
+
* `true` — the default — prints the hours field even when it is
|
|
83
|
+
* `00`, so every value the field writes has the same shape and the
|
|
84
|
+
* same width. `false` rolls the hours into the minutes
|
|
85
|
+
* (`5400000` → `90:00.000`), which the grammar reads back
|
|
86
|
+
* unchanged because the largest field present is unbounded.
|
|
87
|
+
*/
|
|
88
|
+
isHoursShown?: boolean;
|
|
89
|
+
/** `0` through `3`. `0` drops the fraction and its separator. */
|
|
90
|
+
millisecondDigits?: number;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* A position in a piece of media, printed.
|
|
94
|
+
*
|
|
95
|
+
* Zero-padded, and **truncating** rather than rounding: at
|
|
96
|
+
* `millisecondDigits: 0` a position 1999 ms into a file is second 1,
|
|
97
|
+
* because it has not reached second 2 yet. Rounding would print a
|
|
98
|
+
* seek point the media has not played.
|
|
99
|
+
*
|
|
100
|
+
* The default is the canonical `hh:mm:ss.mmm`, and that is what the
|
|
101
|
+
* field writes back on commit. One spelling means a re-read of the
|
|
102
|
+
* field parses to the number that produced it, and a screenshot of
|
|
103
|
+
* the same value is the same picture — neither of which is true of
|
|
104
|
+
* the fleet's five printers, three of which hide the hour and two of
|
|
105
|
+
* which do not pad the minute.
|
|
106
|
+
*/
|
|
107
|
+
export declare const formatTimecode: (milliseconds: number, { isHoursShown, millisecondDigits, }?: FormatTimecodeOptions) => string;
|
|
108
|
+
/**
|
|
109
|
+
* Hold a position inside the media.
|
|
110
|
+
*
|
|
111
|
+
* The floor is `0` when no `minimum` is given, because a media
|
|
112
|
+
* position before the start of the file does not exist — the same
|
|
113
|
+
* reason the grammar refuses a negative rather than clamping one
|
|
114
|
+
* silently. An inverted pair of bounds resolves to the floor: a
|
|
115
|
+
* caller whose `maximum` sits below its `minimum` has a bug, and
|
|
116
|
+
* quietly widening the window would hide it somewhere nobody looks.
|
|
117
|
+
*/
|
|
118
|
+
export declare const clampTimecode: (milliseconds: number, minimum?: number, maximum?: number) => number;
|
|
119
|
+
/**
|
|
120
|
+
* Read a typed timecode, or say why it is not one.
|
|
121
|
+
*
|
|
122
|
+
* Takes no options, deliberately. Every knob considered belongs
|
|
123
|
+
* somewhere else: a maximum is the field's clamp at commit rather
|
|
124
|
+
* than a parse failure (two sources for one fact is how a control
|
|
125
|
+
* ends up invalid with nothing saying why), the decimal comma is
|
|
126
|
+
* accepted unconditionally instead of being a locale switch, and a
|
|
127
|
+
* frame rate makes this a different widget. An options bag that
|
|
128
|
+
* means nothing is API a consumer has to read before learning there
|
|
129
|
+
* is nothing in it.
|
|
130
|
+
*/
|
|
131
|
+
export declare const parseTimecodeInput: (rawText: string) => TimecodeInputResult;
|
|
132
|
+
//# sourceMappingURL=timecode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timecode.d.ts","sourceRoot":"","sources":["../../src/TimecodeInput/timecode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAQH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,IAAI,GAAG,MAAM,CAAA;IAClB,KAAK,EAAE,IAAI,GAAG,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAExC,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,iEAAiE;IACjE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAKD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,GACzB,cAAc,MAAM,EACpB,uCAGG,qBAA0B,KAC5B,MAgCF,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GACxB,cAAc,MAAM,EACpB,UAAU,MAAM,EAChB,UAAU,MAAM,KACf,MAaF,CAAA;AA6HD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,KACd,mBAsEF,CAAA"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a human types into a timecode field, turned into a position
|
|
3
|
+
* in a piece of media — or into a stated reason it was not one.
|
|
4
|
+
*
|
|
5
|
+
* ### Milliseconds, and only milliseconds
|
|
6
|
+
*
|
|
7
|
+
* A media position is a **duration from the start of the file**, so
|
|
8
|
+
* the value is an integer count of milliseconds and nothing else. It
|
|
9
|
+
* is not a `Date`, it is not a clock time, and it is not a string:
|
|
10
|
+
* the only arithmetic anybody does with it is subtraction, and a
|
|
11
|
+
* number is the type that supports it. The props that carry one keep
|
|
12
|
+
* the `Ms` suffix, because a duration's unit is genuinely ambiguous
|
|
13
|
+
* and the fleet has both seconds and milliseconds in use
|
|
14
|
+
* ([decision](../../../../docs/decisions/2026-08-25-an-outbound-http-cache-names-its-fields-fetchedat-and-expiresat.md)).
|
|
15
|
+
*
|
|
16
|
+
* ### The fleet writes the printing half five times and the reading
|
|
17
|
+
* half never
|
|
18
|
+
*
|
|
19
|
+
* `formatTimecode` replaces five hand-rolled `h:mm:ss` printers —
|
|
20
|
+
* queuepilot's `clock` and `toClock`, castkit's `formatTime`, and
|
|
21
|
+
* mux-magic's two modal formatters — each with its own answer to
|
|
22
|
+
* whether the hour is shown and whether the minute is padded.
|
|
23
|
+
* `parseTimecodeInput` has no counterpart at all: nothing in the
|
|
24
|
+
* fleet *reads* a timecode a person typed, which is why the grammar
|
|
25
|
+
* gets the care here rather than being rediscovered per app.
|
|
26
|
+
*
|
|
27
|
+
* ### The rule that governs the grammar: never guess in silence
|
|
28
|
+
*
|
|
29
|
+
* The same three mechanisms `parseDateInput.ts` states, for the same
|
|
30
|
+
* reason:
|
|
31
|
+
*
|
|
32
|
+
* 1. **The grammar is ordered and total.** Each accepting rule is
|
|
33
|
+
* anchored on the *whole* input, the first match wins, and if
|
|
34
|
+
* none matches the answer is a named failure. `1:` is not one
|
|
35
|
+
* minute, and `1:90` is not `2:30`.
|
|
36
|
+
* 2. **An overflow is a failure with a name, not a carry.** `1:90`
|
|
37
|
+
* is refused, and the message offers both legal spellings —
|
|
38
|
+
* `2:30` and `90` — without committing to either. Silently
|
|
39
|
+
* carrying 90 seconds into the minute is the `1:60` trap
|
|
40
|
+
* mux-magic wrote down in
|
|
41
|
+
* `docs/audits/2026-06-29-pre-rename-domain-decisions.md`.
|
|
42
|
+
* 3. **Nothing commits without an echo.** `TimecodeInput` renders
|
|
43
|
+
* the resolved position in full underneath the field, in a live
|
|
44
|
+
* region, before anything commits.
|
|
45
|
+
*
|
|
46
|
+
* ### The largest field present is unbounded; every smaller one is
|
|
47
|
+
* 0–59
|
|
48
|
+
*
|
|
49
|
+
* One sentence covers every bound in the grammar. `90` is ninety
|
|
50
|
+
* **seconds**, `90:00` is ninety **minutes**, and `1:90` is refused
|
|
51
|
+
* because the minute beside it makes 90 a seconds field. That is why
|
|
52
|
+
* a bare number is seconds rather than minutes: seconds is the
|
|
53
|
+
* smallest unit the field prints, so the reading that needs no
|
|
54
|
+
* explanation is the one where the typist appended nothing.
|
|
55
|
+
*/
|
|
56
|
+
const MILLISECONDS_PER_SECOND = 1_000;
|
|
57
|
+
const MILLISECONDS_PER_MINUTE = 60_000;
|
|
58
|
+
const MILLISECONDS_PER_HOUR = 3_600_000;
|
|
59
|
+
const padTwo = (value) => String(value).padStart(2, "0");
|
|
60
|
+
/**
|
|
61
|
+
* A position in a piece of media, printed.
|
|
62
|
+
*
|
|
63
|
+
* Zero-padded, and **truncating** rather than rounding: at
|
|
64
|
+
* `millisecondDigits: 0` a position 1999 ms into a file is second 1,
|
|
65
|
+
* because it has not reached second 2 yet. Rounding would print a
|
|
66
|
+
* seek point the media has not played.
|
|
67
|
+
*
|
|
68
|
+
* The default is the canonical `hh:mm:ss.mmm`, and that is what the
|
|
69
|
+
* field writes back on commit. One spelling means a re-read of the
|
|
70
|
+
* field parses to the number that produced it, and a screenshot of
|
|
71
|
+
* the same value is the same picture — neither of which is true of
|
|
72
|
+
* the fleet's five printers, three of which hide the hour and two of
|
|
73
|
+
* which do not pad the minute.
|
|
74
|
+
*/
|
|
75
|
+
export const formatTimecode = (milliseconds, { isHoursShown = true, millisecondDigits = 3, } = {}) => {
|
|
76
|
+
const total = Number.isFinite(milliseconds)
|
|
77
|
+
? Math.max(0, Math.floor(milliseconds))
|
|
78
|
+
: 0;
|
|
79
|
+
const digits = Math.min(3, Math.max(0, Math.floor(millisecondDigits)));
|
|
80
|
+
const hours = isHoursShown
|
|
81
|
+
? Math.floor(total / MILLISECONDS_PER_HOUR)
|
|
82
|
+
: 0;
|
|
83
|
+
const minutes = Math.floor((total - hours * MILLISECONDS_PER_HOUR) /
|
|
84
|
+
MILLISECONDS_PER_MINUTE);
|
|
85
|
+
const seconds = Math.floor((total % MILLISECONDS_PER_MINUTE) /
|
|
86
|
+
MILLISECONDS_PER_SECOND);
|
|
87
|
+
const fraction = digits === 0
|
|
88
|
+
? ""
|
|
89
|
+
: `.${String(total % MILLISECONDS_PER_SECOND)
|
|
90
|
+
.padStart(3, "0")
|
|
91
|
+
.slice(0, digits)}`;
|
|
92
|
+
return `${isHoursShown ? `${padTwo(hours)}:` : ""}${padTwo(minutes)}:${padTwo(seconds)}${fraction}`;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Hold a position inside the media.
|
|
96
|
+
*
|
|
97
|
+
* The floor is `0` when no `minimum` is given, because a media
|
|
98
|
+
* position before the start of the file does not exist — the same
|
|
99
|
+
* reason the grammar refuses a negative rather than clamping one
|
|
100
|
+
* silently. An inverted pair of bounds resolves to the floor: a
|
|
101
|
+
* caller whose `maximum` sits below its `minimum` has a bug, and
|
|
102
|
+
* quietly widening the window would hide it somewhere nobody looks.
|
|
103
|
+
*/
|
|
104
|
+
export const clampTimecode = (milliseconds, minimum, maximum) => {
|
|
105
|
+
const floor = Math.max(0, minimum ?? 0);
|
|
106
|
+
if (!Number.isFinite(milliseconds)) {
|
|
107
|
+
return floor;
|
|
108
|
+
}
|
|
109
|
+
const ceiling = maximum === undefined || !Number.isFinite(maximum)
|
|
110
|
+
? Number.POSITIVE_INFINITY
|
|
111
|
+
: Math.max(floor, maximum);
|
|
112
|
+
return Math.min(Math.max(milliseconds, floor), ceiling);
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The three accepting rules, in the order they are tried.
|
|
116
|
+
*
|
|
117
|
+
* They are disjoint by colon count, so the order is a statement of
|
|
118
|
+
* discipline rather than a tie-breaker — but the discipline is what
|
|
119
|
+
* keeps a fourth rule from being appended as a nearest guess.
|
|
120
|
+
*
|
|
121
|
+
* The fraction separator may be a `.` or a `,`: half of Europe types
|
|
122
|
+
* the second one, both are unambiguous here because a timecode has
|
|
123
|
+
* no thousands separator, and the echo prints the canonical `.` back
|
|
124
|
+
* either way.
|
|
125
|
+
*/
|
|
126
|
+
const HOURS_MINUTES_SECONDS = /^\+?(\d{1,5}):(\d{1,2}):(\d{1,2})(?:[.,](\d{1,3}))?$/;
|
|
127
|
+
const MINUTES_SECONDS = /^\+?(\d{1,7}):(\d{1,2})(?:[.,](\d{1,3}))?$/;
|
|
128
|
+
const SECONDS = /^\+?(\d{1,9})(?:[.,](\d{1,3}))?$/;
|
|
129
|
+
/** Anchored diagnostics, tried only after every accepting rule has failed. */
|
|
130
|
+
const NEGATIVE = /^-/;
|
|
131
|
+
const TOO_MANY_PARTS = /^\+?\d+(?::\d+){3,}(?:[.,]\d+)?$/;
|
|
132
|
+
const LONG_FRACTION = /^\+?\d+(?::\d+){0,2}[.,]\d{4,}$/;
|
|
133
|
+
const unparsed = (reason) => ({
|
|
134
|
+
kind: "unparsed",
|
|
135
|
+
reason,
|
|
136
|
+
});
|
|
137
|
+
/**
|
|
138
|
+
* A field that overflowed, named rather than carried.
|
|
139
|
+
*
|
|
140
|
+
* The message OFFERS both legal spellings and commits to neither,
|
|
141
|
+
* which is the whole difference between this and a parser that
|
|
142
|
+
* "just works": carrying the overflow moves a clip start by a whole
|
|
143
|
+
* minute and nothing ever says so. Offering the carried form is not
|
|
144
|
+
* the same as taking it — the typist reads two options and picks,
|
|
145
|
+
* and either pick is one keystroke away.
|
|
146
|
+
*/
|
|
147
|
+
const describeOverflow = ({ carriedMilliseconds, dropped, isHoursShown, larger, typed, unit, }) => `${larger} has 60 ${unit}s, so "${typed}" is not a ${unit}s field. Type ${formatTimecode(carriedMilliseconds, { isHoursShown, millisecondDigits: 0 })} to carry it, or ${dropped} to mean ${typed} ${unit}s.`;
|
|
148
|
+
const toMilliseconds = ({ fraction, hours, minutes, seconds, }) => {
|
|
149
|
+
const total = Number(hours ?? 0) * MILLISECONDS_PER_HOUR +
|
|
150
|
+
Number(minutes ?? 0) * MILLISECONDS_PER_MINUTE +
|
|
151
|
+
Number(seconds) * MILLISECONDS_PER_SECOND;
|
|
152
|
+
// The largest field present is unbounded; every smaller one is
|
|
153
|
+
// 0–59. Checked outermost-first, so `1:90:90` reports the minute —
|
|
154
|
+
// the field a reader fixes first.
|
|
155
|
+
if (hours !== undefined &&
|
|
156
|
+
minutes !== undefined &&
|
|
157
|
+
Number(minutes) > 59) {
|
|
158
|
+
return unparsed(describeOverflow({
|
|
159
|
+
carriedMilliseconds: total,
|
|
160
|
+
dropped: `${minutes}:${seconds}`,
|
|
161
|
+
isHoursShown: true,
|
|
162
|
+
larger: "An hour",
|
|
163
|
+
typed: minutes,
|
|
164
|
+
unit: "minute",
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
if (minutes !== undefined && Number(seconds) > 59) {
|
|
168
|
+
return unparsed(describeOverflow({
|
|
169
|
+
carriedMilliseconds: total,
|
|
170
|
+
dropped: seconds,
|
|
171
|
+
isHoursShown: hours !== undefined,
|
|
172
|
+
larger: "A minute",
|
|
173
|
+
typed: seconds,
|
|
174
|
+
unit: "second",
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
// `.5` is half a second, not five milliseconds — a fraction is a
|
|
178
|
+
// decimal fraction of a second, so it pads on the RIGHT.
|
|
179
|
+
return {
|
|
180
|
+
kind: "timecode",
|
|
181
|
+
milliseconds: total +
|
|
182
|
+
(fraction === undefined
|
|
183
|
+
? 0
|
|
184
|
+
: Number(fraction.padEnd(3, "0"))),
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Read a typed timecode, or say why it is not one.
|
|
189
|
+
*
|
|
190
|
+
* Takes no options, deliberately. Every knob considered belongs
|
|
191
|
+
* somewhere else: a maximum is the field's clamp at commit rather
|
|
192
|
+
* than a parse failure (two sources for one fact is how a control
|
|
193
|
+
* ends up invalid with nothing saying why), the decimal comma is
|
|
194
|
+
* accepted unconditionally instead of being a locale switch, and a
|
|
195
|
+
* frame rate makes this a different widget. An options bag that
|
|
196
|
+
* means nothing is API a consumer has to read before learning there
|
|
197
|
+
* is nothing in it.
|
|
198
|
+
*/
|
|
199
|
+
export const parseTimecodeInput = (rawText) => {
|
|
200
|
+
const text = rawText.replace(/\s/g, "");
|
|
201
|
+
if (text === "") {
|
|
202
|
+
return { kind: "empty" };
|
|
203
|
+
}
|
|
204
|
+
// A leading `-` is refused before anything else. Every accepting
|
|
205
|
+
// rule below would otherwise fail on it and report the generic
|
|
206
|
+
// message, which tells a typist that `-5` is unreadable rather
|
|
207
|
+
// than that it is out of bounds.
|
|
208
|
+
if (NEGATIVE.test(text)) {
|
|
209
|
+
return unparsed("A timecode is never negative. The start of the media is 0.");
|
|
210
|
+
}
|
|
211
|
+
const hoursMinutesSeconds = HOURS_MINUTES_SECONDS.exec(text);
|
|
212
|
+
if (hoursMinutesSeconds?.[1] !== undefined &&
|
|
213
|
+
hoursMinutesSeconds[2] !== undefined &&
|
|
214
|
+
hoursMinutesSeconds[3] !== undefined) {
|
|
215
|
+
return toMilliseconds({
|
|
216
|
+
fraction: hoursMinutesSeconds[4],
|
|
217
|
+
hours: hoursMinutesSeconds[1],
|
|
218
|
+
minutes: hoursMinutesSeconds[2],
|
|
219
|
+
seconds: hoursMinutesSeconds[3],
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const minutesSeconds = MINUTES_SECONDS.exec(text);
|
|
223
|
+
if (minutesSeconds?.[1] !== undefined &&
|
|
224
|
+
minutesSeconds[2] !== undefined) {
|
|
225
|
+
return toMilliseconds({
|
|
226
|
+
fraction: minutesSeconds[3],
|
|
227
|
+
minutes: minutesSeconds[1],
|
|
228
|
+
seconds: minutesSeconds[2],
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const seconds = SECONDS.exec(text);
|
|
232
|
+
if (seconds?.[1] !== undefined) {
|
|
233
|
+
return toMilliseconds({
|
|
234
|
+
fraction: seconds[2],
|
|
235
|
+
seconds: seconds[1],
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (TOO_MANY_PARTS.test(text)) {
|
|
239
|
+
return unparsed("A timecode has three parts at most — hours, minutes, seconds. Try 1:02:03.");
|
|
240
|
+
}
|
|
241
|
+
if (LONG_FRACTION.test(text)) {
|
|
242
|
+
return unparsed("The fraction is milliseconds — three digits at most. Try 1:02.500.");
|
|
243
|
+
}
|
|
244
|
+
return unparsed("Not a timecode. Try 90, 1:30, 1:02:03, or 1:02:03.500.");
|
|
245
|
+
};
|
|
246
|
+
//# sourceMappingURL=timecode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timecode.js","sourceRoot":"","sources":["../../src/TimecodeInput/timecode.ts"],"names":[],"mappings":"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"}
|