@charcuterie/ui 4.3.0 → 4.4.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.
@@ -83,6 +83,23 @@ export type TimecodeInputProps = SlotProps & {
83
83
  * discard the boundary just typed. A **zero-length** section is
84
84
  * refused by name: it plays nothing, so swapping would hide a typo
85
85
  * behind a window that silently does not exist.
86
+ *
87
+ * ### The Narrow View stacks the two fields
88
+ *
89
+ * Below `--cq-sm` a section is two fields one above the other, under
90
+ * the captions "Start" and "End"; above it, the row with `to`
91
+ * between them. It is a **container** query, so a 390px modal on a
92
+ * 2560px monitor gets the stacked form that a window breakpoint
93
+ * would have denied it. The captions are `aria-hidden`, so the
94
+ * accessible name of each control is the same in both layouts.
95
+ *
96
+ * ### Milliseconds are optional to read as well as to type
97
+ *
98
+ * The grammar has always accepted `1:01:00`. The field now writes it
99
+ * back as `01:01:00` rather than as `01:01:00.000`, through
100
+ * `formatTimecode`'s `millisecondDigits: "auto"`, because a fraction
101
+ * nobody typed is not information. `1:01:00.5` still commits as
102
+ * `01:01:00.500`.
86
103
  */
87
104
  export declare const TimecodeInput: ({ className, durationMs, isDisabled, isRange, label, maxValueMs, minValueMs, onChange, placeholder, size, stepMs, valueMs, ...receivedSlotProps }: TimecodeInputProps) => ReactNode;
88
105
  //# sourceMappingURL=TimecodeInput.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TimecodeInput.d.ts","sourceRoot":"","sources":["../../src/TimecodeInput/TimecodeInput.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAEV,SAAS,EACV,MAAM,OAAO,CAAA;AAQd,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAGhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAOlD,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,GAAG,aAAa,KAAK,IAAI,CAAA;IACzD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,GAAG,aAAa,CAAA;CACjC,CAAA;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,eAAO,MAAM,aAAa,GAAI,mJAc3B,kBAAkB,KAAG,SAsWvB,CAAA"}
1
+ {"version":3,"file":"TimecodeInput.d.ts","sourceRoot":"","sources":["../../src/TimecodeInput/TimecodeInput.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAEV,SAAS,EACV,MAAM,OAAO,CAAA;AAQd,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAGhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAOlD,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,GAAG,aAAa,KAAK,IAAI,CAAA;IACzD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,GAAG,aAAa,CAAA;CACjC,CAAA;AA+BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,eAAO,MAAM,aAAa,GAAI,mJAc3B,kBAAkB,KAAG,SAqbvB,CAAA"}
@@ -1,5 +1,5 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
1
  import { createElement as _createElement } from "react";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useUniqueId } from "@charcuterie/logic";
4
4
  import { useState } from "react";
5
5
  import { CONTROL_SIZE_CLASS } from "../controlStyles.js";
@@ -8,6 +8,20 @@ import { mergeSlotProps } from "../slotProps.js";
8
8
  import { toClassName } from "../toClassName.js";
9
9
  import { clampTimecode, formatTimecode, parseTimecodeInput, } from "./timecode.js";
10
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
+ /**
12
+ * How this field prints, everywhere it prints — the text it writes
13
+ * back on commit, and the echo that reports what a draft resolves
14
+ * to. The two have to agree, so they read the same constant.
15
+ *
16
+ * `"auto"` rather than the exported default of three digits: a
17
+ * person who types `1:01:00` and tabs away did not ask for
18
+ * `01:01:00.000`, any more than typing `3` asks for `3.000`. A real
19
+ * fraction still prints in full, and the grammar reads both back to
20
+ * the same number.
21
+ */
22
+ const DISPLAY_FORMAT = {
23
+ millisecondDigits: "auto",
24
+ };
11
25
  const toSeededRange = (valueMs) => valueMs === undefined
12
26
  ? { end: null, start: null }
13
27
  : typeof valueMs === "number"
@@ -57,6 +71,23 @@ const toSeededRange = (valueMs) => valueMs === undefined
57
71
  * discard the boundary just typed. A **zero-length** section is
58
72
  * refused by name: it plays nothing, so swapping would hide a typo
59
73
  * behind a window that silently does not exist.
74
+ *
75
+ * ### The Narrow View stacks the two fields
76
+ *
77
+ * Below `--cq-sm` a section is two fields one above the other, under
78
+ * the captions "Start" and "End"; above it, the row with `to`
79
+ * between them. It is a **container** query, so a 390px modal on a
80
+ * 2560px monitor gets the stacked form that a window breakpoint
81
+ * would have denied it. The captions are `aria-hidden`, so the
82
+ * accessible name of each control is the same in both layouts.
83
+ *
84
+ * ### Milliseconds are optional to read as well as to type
85
+ *
86
+ * The grammar has always accepted `1:01:00`. The field now writes it
87
+ * back as `01:01:00` rather than as `01:01:00.000`, through
88
+ * `formatTimecode`'s `millisecondDigits: "auto"`, because a fraction
89
+ * nobody typed is not information. `1:01:00.5` still commits as
90
+ * `01:01:00.500`.
60
91
  */
61
92
  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
93
  const baseId = useUniqueId();
@@ -66,12 +97,24 @@ export const TimecodeInput = ({ className, durationMs, isDisabled = false, isRan
66
97
  const [range, setRange] = useState(seededRange);
67
98
  const toDisplayText = (milliseconds) => milliseconds === null
68
99
  ? ""
69
- : formatTimecode(milliseconds);
100
+ : formatTimecode(milliseconds, DISPLAY_FORMAT);
70
101
  const [texts, setTexts] = useState({
71
102
  end: toDisplayText(seededRange.end),
72
103
  start: toDisplayText(seededRange.start),
73
104
  });
105
+ /**
106
+ * Which endpoint the keyboard steps and the echo belong to. It
107
+ * SURVIVES blur, deliberately: a refusal has to keep naming the
108
+ * field it is about after the focus has gone somewhere else.
109
+ */
74
110
  const [activeEndpoint, setActiveEndpoint] = useState("start");
111
+ /**
112
+ * Whether a field of this control holds the focus. Separate from
113
+ * `activeEndpoint` because the two answer different questions —
114
+ * *which* endpoint, and *whether anybody is typing* — and only the
115
+ * second one may hide the echo.
116
+ */
117
+ const [isFocused, setIsFocused] = useState(false);
75
118
  /**
76
119
  * A refusal the grammar cannot see, because it is about the
77
120
  * **pair** rather than about either text: a zero-length section.
@@ -191,10 +234,39 @@ export const TimecodeInput = ({ className, durationMs, isDisabled = false, isRan
191
234
  : parsed.kind === "empty" || clampedPreview === null
192
235
  ? ""
193
236
  : 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;
237
+ ? formatTimecode(parsed.milliseconds, DISPLAY_FORMAT)
238
+ : `${formatTimecode(parsed.milliseconds, DISPLAY_FORMAT)} is outside the media, so it commits as ${formatTimecode(clampedPreview, DISPLAY_FORMAT)}.`;
239
+ /**
240
+ * A restatement is a reading of the field above it, so it leaves
241
+ * with the focus. Left up, an unlabelled `00:05:00` under an idle
242
+ * control reads as a stray value rather than as a report on one.
243
+ *
244
+ * A REFUSAL stays, and both kinds do. `rangeRefusal` is set on
245
+ * commit — which is exactly blur — so a rule that hid it on blur
246
+ * would mean it was never readable at all. And an unparsed text
247
+ * keeps `aria-invalid` on the control, so the sentence saying why
248
+ * has to stay with it: an invalid field with no message is worse
249
+ * than the stray line this removes.
250
+ *
251
+ * Emptying a live region announces nothing, so the blur costs a
252
+ * screen reader no chatter.
253
+ */
254
+ const echoText = rangeRefusal ??
255
+ (isFocused || parsed.kind === "unparsed"
256
+ ? parsedEcho
257
+ : "");
197
258
  const isInvalid = rangeRefusal !== null || parsed.kind === "unparsed";
259
+ /**
260
+ * The Narrow View's caption over one field.
261
+ *
262
+ * Presentational, and `aria-hidden` for the same reason the word
263
+ * `to` is: each input already carries
264
+ * `aria-label="<label> <endpoint>"`, and a real `<label>` here
265
+ * would name the start control a second time and fight the `id` a
266
+ * `Field` above clones onto it. The eye gets a caption; the
267
+ * accessible name is the same in both layouts.
268
+ */
269
+ const renderCaption = (text) => (_jsx("span", { "aria-hidden": "true", className: "text-content-secondary text-sm cq-sm:hidden", children: text }));
198
270
  const renderInput = (endpoint) => {
199
271
  const ownProps = {
200
272
  "aria-describedby": echoId,
@@ -223,6 +295,7 @@ export const TimecodeInput = ({ className, durationMs, isDisabled = false, isRan
223
295
  // rather than by a keyboard the field cannot choose.
224
296
  inputMode: "numeric",
225
297
  onBlur: () => {
298
+ setIsFocused(false);
226
299
  commitText(endpoint);
227
300
  },
228
301
  onChange: (changeEvent) => {
@@ -238,6 +311,7 @@ export const TimecodeInput = ({ className, durationMs, isDisabled = false, isRan
238
311
  }));
239
312
  },
240
313
  onFocus: () => {
314
+ setIsFocused(true);
241
315
  setActiveEndpoint(endpoint);
242
316
  },
243
317
  onKeyDown: (keyEvent) => {
@@ -251,7 +325,7 @@ export const TimecodeInput = ({ className, durationMs, isDisabled = false, isRan
251
325
  ? mergeSlotProps(receivedSlotProps, ownProps)
252
326
  : ownProps), key: endpoint }));
253
327
  };
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
328
+ return (_jsxs("div", { className: toClassName("@container flex flex-col gap-1", className), children: [isRange ? (_jsxs("div", { className: "flex flex-col gap-2 cq-sm:flex-row cq-sm:items-center cq-sm:gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-1 cq-sm:flex-1", children: [renderCaption("Start"), renderInput("start")] }), _jsx("span", { "aria-hidden": "true", className: "hidden shrink-0 text-content-secondary text-sm cq-sm:inline", children: "to" }), _jsxs("div", { className: "flex min-w-0 flex-col gap-1 cq-sm:flex-1", children: [renderCaption("End"), renderInput("end")] })] })) : (renderInput("start")), _jsx("p", { "aria-live": "polite", className: toClassName("min-h-5 text-sm", isInvalid
255
329
  ? "text-intent-danger-content"
256
330
  : "text-content-secondary"), id: echoId, role: "status", children: echoText })] }));
257
331
  };
@@ -1 +1 @@
1
- {"version":3,"file":"TimecodeInput.js","sourceRoot":"","sources":["../../src/TimecodeInput/TimecodeInput.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TimecodeInput.js","sourceRoot":"","sources":["../../src/TimecodeInput/TimecodeInput.tsx"],"names":[],"mappings":"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"}
@@ -86,8 +86,17 @@ export type FormatTimecodeOptions = {
86
86
  * unchanged because the largest field present is unbounded.
87
87
  */
88
88
  isHoursShown?: boolean;
89
- /** `0` through `3`. `0` drops the fraction and its separator. */
90
- millisecondDigits?: number;
89
+ /**
90
+ * `0` through `3`. `0` drops the fraction and its separator.
91
+ *
92
+ * `"auto"` prints the fraction only when there is one to print: a
93
+ * whole number of seconds comes back as `01:01:00`, and half a
94
+ * second more comes back as `01:01:00.500`. It is how a decimal
95
+ * number is written everywhere else — you write `3`, not `3.000` —
96
+ * and the grammar reads both spellings back to the number that
97
+ * produced them, because its fraction is optional.
98
+ */
99
+ millisecondDigits?: "auto" | number;
91
100
  };
92
101
  /**
93
102
  * A position in a piece of media, printed.
@@ -97,12 +106,16 @@ export type FormatTimecodeOptions = {
97
106
  * because it has not reached second 2 yet. Rounding would print a
98
107
  * seek point the media has not played.
99
108
  *
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.
109
+ * The default is the canonical `hh:mm:ss.mmm`. `TimecodeInput` asks
110
+ * for `millisecondDigits: "auto"` instead, so a whole second is
111
+ * written back as `01:01:00` rather than as `01:01:00.000` — a
112
+ * person who typed no fraction did not ask for one, and both
113
+ * spellings re-read to the number that produced them.
114
+ *
115
+ * Either way it is ONE spelling per value, which is what the fleet's
116
+ * five printers never were: three of them hide the hour and two do
117
+ * not pad the minute, so the same position is a different picture in
118
+ * each app.
106
119
  */
107
120
  export declare const formatTimecode: (milliseconds: number, { isHoursShown, millisecondDigits, }?: FormatTimecodeOptions) => string;
108
121
  /**
@@ -1 +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"}
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;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CACpC,CAAA;AAKD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,cAAc,GACzB,cAAc,MAAM,EACpB,uCAGG,qBAA0B,KAC5B,MAqCF,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"}
@@ -65,18 +65,26 @@ const padTwo = (value) => String(value).padStart(2, "0");
65
65
  * because it has not reached second 2 yet. Rounding would print a
66
66
  * seek point the media has not played.
67
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.
68
+ * The default is the canonical `hh:mm:ss.mmm`. `TimecodeInput` asks
69
+ * for `millisecondDigits: "auto"` instead, so a whole second is
70
+ * written back as `01:01:00` rather than as `01:01:00.000` — a
71
+ * person who typed no fraction did not ask for one, and both
72
+ * spellings re-read to the number that produced them.
73
+ *
74
+ * Either way it is ONE spelling per value, which is what the fleet's
75
+ * five printers never were: three of them hide the hour and two do
76
+ * not pad the minute, so the same position is a different picture in
77
+ * each app.
74
78
  */
75
79
  export const formatTimecode = (milliseconds, { isHoursShown = true, millisecondDigits = 3, } = {}) => {
76
80
  const total = Number.isFinite(milliseconds)
77
81
  ? Math.max(0, Math.floor(milliseconds))
78
82
  : 0;
79
- const digits = Math.min(3, Math.max(0, Math.floor(millisecondDigits)));
83
+ const digits = millisecondDigits === "auto"
84
+ ? total % MILLISECONDS_PER_SECOND === 0
85
+ ? 0
86
+ : 3
87
+ : Math.min(3, Math.max(0, Math.floor(millisecondDigits)));
80
88
  const hours = isHoursShown
81
89
  ? Math.floor(total / MILLISECONDS_PER_HOUR)
82
90
  : 0;
@@ -1 +1 @@
1
- {"version":3,"file":"timecode.js","sourceRoot":"","sources":["../../src/TimecodeInput/timecode.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"timecode.js","sourceRoot":"","sources":["../../src/TimecodeInput/timecode.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@charcuterie/ui",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "The Charcuterie components. Tokens for look, @charcuterie/logic for state, ARIA that agents can actually drive.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -74,11 +74,27 @@ Three mechanisms keep a grammar that permissive from being dangerous:
74
74
  the field, in a `role="status"` live region wired into the input's `aria-describedby`.
75
75
  Commit is Enter or blur; a keystroke never commits.
76
76
 
77
+ The echo is a **reading of the focused field**, so it leaves when the focus does. A
78
+ restatement left under an idle control is an unlabelled timecode floating on the page, which
79
+ reads as a value rather than as a report on the one above it.
80
+
81
+ A **refusal** is the other case and it stays up, both kinds of it. The zero-length complaint
82
+ is written at the commit — which *is* the blur — so a rule that cleared it on blur would mean
83
+ it was never readable. And a text the grammar cannot read keeps `aria-invalid` on the
84
+ control, so the sentence saying why has to stay with it: an invalid field with no message is
85
+ worse than the stray line this removes. Emptying a live region announces nothing, so none of
86
+ this makes a screen reader chatty.
87
+
77
88
  **The largest field present is unbounded; every smaller one is 0–59.** That one sentence is
78
89
  every bound in the grammar. It is also why a bare number is seconds: seconds is the smallest
79
90
  unit the field prints, so the reading that needs no explanation is the one where the typist
80
91
  appended nothing.
81
92
 
93
+ **Milliseconds are optional to read as well as to type.** The grammar has always accepted
94
+ `1:01:00`, and the field now writes it back as `01:01:00` rather than as `01:01:00.000` — you
95
+ write 3, not 3.000. A real fraction still prints in full: `1:01:00.5` commits as
96
+ `01:01:00.500`, because a fraction pads on the right.
97
+
82
98
  Refused by name: a negative, a seconds or minutes field of 60 or more with a larger field
83
99
  beside it, more than three colon-separated parts, a fraction longer than three digits, and
84
100
  anything the accepting rules do not match. A refused value leaves the text **exactly as
@@ -132,6 +148,25 @@ its text is refused — which are exactly the states the echo exists for. A spin
132
148
  value is a widget that under-reports itself to a screen reader in its two most important
133
149
  states.
134
150
 
151
+ ## The Narrow View stacks the two fields
152
+
153
+ A section is `[start] to [end]` in a row while there is room for it. Below `--cq-sm` the two
154
+ fields **stack**, and the `to` is replaced by a "Start" caption over the first and an "End"
155
+ caption over the second. Two `hh:mm:ss.mmm` fields and a word between them fill a 390px modal
156
+ edge to edge with no slack, which is where this was reported from.
157
+
158
+ It is a **container** query, not a window breakpoint. The box that ran out of room is this
159
+ field's own, and a 390px dialog on a 2560px monitor is the Wide View by every media query
160
+ ever written. The cost of `container-type: inline-size` is the `contain: inline-size` that
161
+ rides with it, so `TimecodeInput` takes its width from its parent rather than from its
162
+ content — give it a definite inline size, as with every container-query component here.
163
+
164
+ The captions are `aria-hidden`, exactly as the `to` always was. Each input already carries
165
+ `aria-label="<label> start"` / `"<label> end"`, and a real `<label>` would name the start
166
+ control a second time and fight the `id` a `Field` above clones onto it. **The accessible
167
+ name of each control is identical in both layouts**, and `TimecodeInput.test.tsx` asserts it
168
+ at two container widths.
169
+
135
170
  <Canvas of={TimecodeInputStories.Responsive} />
136
171
 
137
172
  ## The formatter is exported, because the fleet writes it repeatedly
@@ -143,13 +178,18 @@ whether the hour is shown and whether the minute is padded. A read-only surface
143
178
  the exported one rather than spell a sixth.
144
179
 
145
180
  ```ts
146
- formatTimecode(3_723_500) // "01:02:03.500"
147
- formatTimecode(90_000, { isHoursShown: false }) // "01:30.000"
148
- formatTimecode(1_999, { millisecondDigits: 0 }) // "00:00:01" — truncates
181
+ formatTimecode(3_723_500) // "01:02:03.500"
182
+ formatTimecode(90_000, { isHoursShown: false }) // "01:30.000"
183
+ formatTimecode(1_999, { millisecondDigits: 0 }) // "00:00:01" — truncates
184
+ formatTimecode(3_660_000, { millisecondDigits: "auto" }) // "01:01:00" — no fraction to print
185
+ formatTimecode(3_660_500, { millisecondDigits: "auto" }) // "01:01:00.500"
149
186
  ```
150
187
 
151
- The default is the canonical `hh:mm:ss.mmm`, and it is what the field writes back on commit:
152
- one spelling means a re-read of the field parses to the number that produced it, and a
153
- screenshot of the same value is the same picture.
188
+ The **default is the canonical `hh:mm:ss.mmm`** and it stays that way, because a published
189
+ export is read by callers no change here can see. `TimecodeInput` opts in to `"auto"` for
190
+ both the text it writes back and its own echo, so the two agree on the page.
191
+
192
+ Either way it is one spelling per value: a re-read of the field parses to the number that
193
+ produced it, and a screenshot of the same value is the same picture.
154
194
 
155
195
  <Canvas of={TimecodeInputStories.Interactive} />
@@ -20,6 +20,12 @@ import { formatTimecode } from "./timecode.ts"
20
20
  */
21
21
  const DURATION_MS = 2_712_000
22
22
 
23
+ /**
24
+ * The demo readout prints a committed value the way the field
25
+ * writes it — `millisecondDigits: "auto"` — so one position is one
26
+ * spelling everywhere on the page. `formatTimecode`'s own default is
27
+ * still the full `hh:mm:ss.mmm`, and the docs page shows both.
28
+ */
23
29
  const describeValue = (
24
30
  value: null | number | TimecodeRange,
25
31
  ) => {
@@ -28,7 +34,9 @@ const describeValue = (
28
34
  }
29
35
 
30
36
  if (typeof value === "number") {
31
- return formatTimecode(value)
37
+ return formatTimecode(value, {
38
+ millisecondDigits: "auto",
39
+ })
32
40
  }
33
41
 
34
42
  // A section with neither end set is the same absence `onChange`
@@ -37,7 +45,7 @@ const describeValue = (
37
45
  return "—"
38
46
  }
39
47
 
40
- return `${value.start === null ? "the beginning" : formatTimecode(value.start)} to ${value.end === null ? "the end" : formatTimecode(value.end)}`
48
+ return `${value.start === null ? "the beginning" : formatTimecode(value.start, { millisecondDigits: "auto" })} to ${value.end === null ? "the end" : formatTimecode(value.end, { millisecondDigits: "auto" })}`
41
49
  }
42
50
 
43
51
  const meta = {
@@ -97,6 +105,13 @@ const TimecodeHarness = ({
97
105
  * it before anything commits — `90` is ninety seconds, `1:30` is the
98
106
  * same position spelled the other way, `1:02:03.500` is the full
99
107
  * form. Enter or blur commits; Escape puts the last value back.
108
+ *
109
+ * The line is a reading of the field, so it leaves when the focus
110
+ * does. A refusal stays up, because a control that is still marked
111
+ * invalid needs the sentence that says why.
112
+ *
113
+ * Milliseconds are optional both ways: `1:01:00` commits as
114
+ * `01:01:00`, not as `01:01:00.000`.
100
115
  */
101
116
  export const Default: Story = {
102
117
  args: { label: "Start at" },
@@ -257,9 +272,14 @@ export const AllStates: Story = {
257
272
  }
258
273
 
259
274
  /**
260
- * The field at three container widths. Two inputs and a word between
261
- * them is the layout that runs out of room first, so the section
262
- * mode is the one worth looking at narrow.
275
+ * The field at three container widths, which is the only honest way
276
+ * to story a container query — the window never moves.
277
+ *
278
+ * Below `--cq-sm` the section becomes the **Narrow View**: the two
279
+ * fields stack, and the word `to` is replaced by a "Start" caption
280
+ * over the first and an "End" caption over the second. Two
281
+ * `hh:mm:ss.mmm` fields and a word between them fill a phone-width
282
+ * modal edge to edge, which is where this was reported from.
263
283
  */
264
284
  export const Responsive: Story = {
265
285
  args: { label: "Start at" },
@@ -286,7 +306,9 @@ export const Responsive: Story = {
286
306
  *
287
307
  * In the section below, type an end that sits before the start and
288
308
  * the pair **swaps**; type an end equal to the start and it is
289
- * **refused**, because a section with no length plays nothing.
309
+ * **refused**, because a section with no length plays nothing. The
310
+ * refusal is readable after the field has been left, which is when
311
+ * a commit-time complaint is written.
290
312
  */
291
313
  export const Interactive: Story = {
292
314
  args: { label: "Start at" },
@@ -7,7 +7,8 @@ import { mountStory } from "../mountStory.testHelpers.ts"
7
7
  import { expectAgentDrivable } from "../testing/index.ts"
8
8
  import * as stories from "./TimecodeInput.stories.tsx"
9
9
 
10
- const { AllStates, Interactive } = composeStories(stories)
10
+ const { AllStates, Interactive, Responsive } =
11
+ composeStories(stories)
11
12
 
12
13
  const mountInteractive = async () => {
13
14
  const mounted = await mountStory(Interactive)
@@ -40,9 +41,7 @@ test("a typed position is echoed in full, and does NOT commit until Enter", asyn
40
41
  // and the field says so in a live region before anything is
41
42
  // stored.
42
43
  await waitFor(() => {
43
- expect(
44
- canvas.getByText("00:01:30.000"),
45
- ).toBeInTheDocument()
44
+ expect(canvas.getByText("00:01:30")).toBeInTheDocument()
46
45
  })
47
46
 
48
47
  await expect(
@@ -53,13 +52,13 @@ test("a typed position is echoed in full, and does NOT commit until Enter", asyn
53
52
 
54
53
  await waitFor(() => {
55
54
  expect(
56
- canvas.getByText("Chosen: 00:01:30.000"),
55
+ canvas.getByText("Chosen: 00:01:30"),
57
56
  ).toBeInTheDocument()
58
57
  })
59
58
 
60
59
  // Committed in the one canonical spelling, so a re-read of the
61
60
  // field parses to the number that produced it.
62
- await expect(single).toHaveValue("00:01:30.000")
61
+ await expect(single).toHaveValue("00:01:30")
63
62
  })
64
63
 
65
64
  test("an overflowed field is refused by name, and the typed text survives", async () => {
@@ -101,7 +100,7 @@ test("Escape puts the last committed value back", async () => {
101
100
 
102
101
  await userEvent.type(single, "90{Enter}")
103
102
 
104
- await expect(single).toHaveValue("00:01:30.000")
103
+ await expect(single).toHaveValue("00:01:30")
105
104
 
106
105
  await userEvent.clear(single)
107
106
 
@@ -109,7 +108,7 @@ test("Escape puts the last committed value back", async () => {
109
108
 
110
109
  await userEvent.keyboard("{Escape}")
111
110
 
112
- await expect(single).toHaveValue("00:01:30.000")
111
+ await expect(single).toHaveValue("00:01:30")
113
112
  })
114
113
 
115
114
  test("the arrows step by stepMs, and Shift multiplies that by ten", async () => {
@@ -123,24 +122,24 @@ test("the arrows step by stepMs, and Shift multiplies that by ten", async () =>
123
122
  // than a draft the echo is reporting on.
124
123
  await waitFor(() => {
125
124
  expect(
126
- canvas.getByText("Chosen: 00:00:01.000"),
125
+ canvas.getByText("Chosen: 00:00:01"),
127
126
  ).toBeInTheDocument()
128
127
  })
129
128
 
130
129
  await userEvent.keyboard("{Shift>}{ArrowUp}{/Shift}")
131
130
 
132
- await expect(single).toHaveValue("00:00:11.000")
131
+ await expect(single).toHaveValue("00:00:11")
133
132
 
134
133
  await userEvent.keyboard("{ArrowDown}")
135
134
 
136
- await expect(single).toHaveValue("00:00:10.000")
135
+ await expect(single).toHaveValue("00:00:10")
137
136
 
138
137
  // The floor is the start of the media, not a negative position.
139
138
  await userEvent.keyboard(
140
139
  "{Shift>}{ArrowDown}{ArrowDown}{/Shift}",
141
140
  )
142
141
 
143
- await expect(single).toHaveValue("00:00:00.000")
142
+ await expect(single).toHaveValue("00:00:00")
144
143
  })
145
144
 
146
145
  test("a position past the end of the media says so, then commits clamped", async () => {
@@ -152,13 +151,13 @@ test("a position past the end of the media says so, then commits clamped", async
152
151
 
153
152
  await waitFor(() => {
154
153
  expect(
155
- canvas.getByText(/commits as 00:45:12\.000/),
154
+ canvas.getByText(/commits as 00:45:12/),
156
155
  ).toBeInTheDocument()
157
156
  })
158
157
 
159
158
  await userEvent.keyboard("{Enter}")
160
159
 
161
- await expect(single).toHaveValue("00:45:12.000")
160
+ await expect(single).toHaveValue("00:45:12")
162
161
  })
163
162
 
164
163
  test("an inverted section swaps, rather than throwing away the boundary just typed", async () => {
@@ -167,14 +166,12 @@ test("an inverted section swaps, rather than throwing away the boundary just typ
167
166
 
168
167
  await userEvent.click(end)
169
168
 
170
- // The seeded start is 00:05:00.000, so this end sits before it.
169
+ // The seeded start is 00:05:00, so this end sits before it.
171
170
  await userEvent.type(end, "1:00{Enter}")
172
171
 
173
172
  await waitFor(() => {
174
173
  expect(
175
- canvas.getByText(
176
- "Chosen: 00:01:00.000 to 00:05:00.000",
177
- ),
174
+ canvas.getByText("Chosen: 00:01:00 to 00:05:00"),
178
175
  ).toBeInTheDocument()
179
176
  })
180
177
 
@@ -200,7 +197,7 @@ test("a zero-length section is refused by name, because it plays nothing", async
200
197
  // The start is untouched and the end is still open — nothing was
201
198
  // committed, and the text is still what was typed.
202
199
  await expect(
203
- canvas.getByText("Chosen: 00:05:00.000 to the end"),
200
+ canvas.getByText("Chosen: 00:05:00 to the end"),
204
201
  ).toBeInTheDocument()
205
202
 
206
203
  await expect(end).toHaveValue("5:00")
@@ -217,9 +214,7 @@ test("both ends are independently optional, and clearing one leaves the other al
217
214
 
218
215
  await waitFor(() => {
219
216
  expect(
220
- canvas.getByText(
221
- "Chosen: 00:05:00.000 to 00:10:00.000",
222
- ),
217
+ canvas.getByText("Chosen: 00:05:00 to 00:10:00"),
223
218
  ).toBeInTheDocument()
224
219
  })
225
220
 
@@ -231,9 +226,7 @@ test("both ends are independently optional, and clearing one leaves the other al
231
226
 
232
227
  await waitFor(() => {
233
228
  expect(
234
- canvas.getByText(
235
- "Chosen: the beginning to 00:10:00.000",
236
- ),
229
+ canvas.getByText("Chosen: the beginning to 00:10:00"),
237
230
  ).toBeInTheDocument()
238
231
  })
239
232
  })
@@ -260,20 +253,250 @@ test("the four states of a section all render as values, not as half-finished in
260
253
  ).toHaveLength(2)
261
254
 
262
255
  await expect(
263
- canvas.getByText("Chosen: 00:05:00.000 to the end"),
256
+ canvas.getByText("Chosen: 00:05:00 to the end"),
264
257
  ).toBeInTheDocument()
265
258
 
266
259
  await expect(
267
- canvas.getByText(
268
- "Chosen: the beginning to 00:05:00.000",
269
- ),
260
+ canvas.getByText("Chosen: the beginning to 00:05:00"),
270
261
  ).toBeInTheDocument()
271
262
 
272
263
  await expect(
273
- canvas.getByText(
274
- "Chosen: 00:05:00.000 to 00:15:00.000",
275
- ),
264
+ canvas.getByText("Chosen: 00:05:00 to 00:15:00"),
265
+ ).toBeInTheDocument()
266
+
267
+ await expectNoAxeViolations(canvasElement)
268
+ })
269
+
270
+ test("the echo is a reading of the focused field, so it leaves with the focus", async () => {
271
+ const { canvas, single } = await mountInteractive()
272
+
273
+ await userEvent.click(single)
274
+
275
+ await userEvent.type(single, "90")
276
+
277
+ await waitFor(() => {
278
+ expect(canvas.getByText("00:01:30")).toBeInTheDocument()
279
+ })
280
+
281
+ // Tab commits and takes the focus away. What is left underneath an
282
+ // idle field would be an unlabelled timecode, which reads as a
283
+ // stray value rather than as a report on the field above it.
284
+ await userEvent.tab()
285
+
286
+ await waitFor(() => {
287
+ expect(
288
+ canvas.queryByText("00:01:30"),
289
+ ).not.toBeInTheDocument()
290
+ })
291
+
292
+ // The value itself is untouched by any of that: the line is a
293
+ // reading, not the state.
294
+ await expect(single).toHaveValue("00:01:30")
295
+
296
+ await expect(
297
+ canvas.getByText("Chosen: 00:01:30"),
298
+ ).toBeInTheDocument()
299
+ })
300
+
301
+ test("an unparsed text keeps its refusal after the focus leaves, because the control is still invalid", async () => {
302
+ const { canvas, canvasElement, single } =
303
+ await mountInteractive()
304
+
305
+ await userEvent.click(single)
306
+
307
+ await userEvent.type(single, "1:90")
308
+
309
+ await userEvent.tab()
310
+
311
+ // `aria-invalid` survives the blur, so the sentence that says why
312
+ // has to survive it too. An invalid control with no message is
313
+ // worse than the stray line the test above removes.
314
+ await expect(single).toHaveAttribute(
315
+ "aria-invalid",
316
+ "true",
317
+ )
318
+
319
+ await expect(
320
+ canvas.getByText(/A minute has 60 seconds/),
276
321
  ).toBeInTheDocument()
277
322
 
323
+ await expect(single).toHaveValue("1:90")
324
+
278
325
  await expectNoAxeViolations(canvasElement)
279
326
  })
327
+
328
+ test("a zero-length refusal is written at the commit, so it survives the blur that wrote it", async () => {
329
+ const { canvas, canvasElement, end } =
330
+ await mountInteractive()
331
+
332
+ await userEvent.click(end)
333
+
334
+ // The seeded start is 00:05:00, so this end has no length. Blur is
335
+ // the commit here, which is the case that would have hidden the
336
+ // complaint the instant it was made.
337
+ await userEvent.type(end, "5:00")
338
+
339
+ await userEvent.tab()
340
+
341
+ await waitFor(() => {
342
+ expect(
343
+ canvas.getByText(/A section needs a length/),
344
+ ).toBeInTheDocument()
345
+ })
346
+
347
+ await expect(end).toHaveAttribute("aria-invalid", "true")
348
+
349
+ await expectNoAxeViolations(canvasElement)
350
+ })
351
+
352
+ /**
353
+ * The container query, asserted rather than assumed — and the
354
+ * accessible name held identical across the two layouts it draws.
355
+ *
356
+ * The window never moves in this test. Each panel of the board is a
357
+ * fixed inline size around `--cq-sm`, which is the only honest way
358
+ * to exercise a query about the component's own box.
359
+ */
360
+ test("the Narrow View stacks the two fields, and neither layout changes a control's name", async () => {
361
+ const { canvas, canvasElement } =
362
+ await mountStory(Responsive)
363
+
364
+ const narrowStart = expectAgentDrivable(canvas, {
365
+ name: "Section 15rem start",
366
+ role: "textbox",
367
+ })
368
+
369
+ const wideStart = expectAgentDrivable(canvas, {
370
+ name: "Section 34rem start",
371
+ role: "textbox",
372
+ })
373
+
374
+ expectAgentDrivable(canvas, {
375
+ name: "Section 15rem end",
376
+ role: "textbox",
377
+ })
378
+
379
+ expectAgentDrivable(canvas, {
380
+ name: "Section 34rem end",
381
+ role: "textbox",
382
+ })
383
+
384
+ const narrowRow = narrowStart.parentElement
385
+ ?.parentElement as HTMLElement
386
+
387
+ const wideRow = wideStart.parentElement
388
+ ?.parentElement as HTMLElement
389
+
390
+ await expect(
391
+ globalThis.getComputedStyle(narrowRow).flexDirection,
392
+ ).toBe("column")
393
+
394
+ await expect(
395
+ globalThis.getComputedStyle(wideRow).flexDirection,
396
+ ).toBe("row")
397
+
398
+ const [
399
+ narrowStartCaption,
400
+ narrowJoiner,
401
+ narrowEndCaption,
402
+ ] = Array.from(
403
+ narrowRow.querySelectorAll<HTMLElement>(
404
+ ":scope > * > span, :scope > span",
405
+ ),
406
+ )
407
+
408
+ const [wideStartCaption, wideJoiner, wideEndCaption] =
409
+ Array.from(
410
+ wideRow.querySelectorAll<HTMLElement>(
411
+ ":scope > * > span, :scope > span",
412
+ ),
413
+ )
414
+
415
+ // Narrow: two captions, no joining word.
416
+ await expect(narrowStartCaption).toHaveTextContent(
417
+ "Start",
418
+ )
419
+
420
+ await expect(narrowEndCaption).toHaveTextContent("End")
421
+
422
+ await expect(
423
+ globalThis.getComputedStyle(
424
+ narrowStartCaption as HTMLElement,
425
+ ).display,
426
+ ).not.toBe("none")
427
+
428
+ await expect(
429
+ globalThis.getComputedStyle(narrowJoiner as HTMLElement)
430
+ .display,
431
+ ).toBe("none")
432
+
433
+ // Wide: the joining word, no captions.
434
+ await expect(wideJoiner).toHaveTextContent("to")
435
+
436
+ await expect(
437
+ globalThis.getComputedStyle(wideJoiner as HTMLElement)
438
+ .display,
439
+ ).not.toBe("none")
440
+
441
+ await expect(
442
+ globalThis.getComputedStyle(
443
+ wideStartCaption as HTMLElement,
444
+ ).display,
445
+ ).toBe("none")
446
+
447
+ await expect(
448
+ globalThis.getComputedStyle(
449
+ wideEndCaption as HTMLElement,
450
+ ).display,
451
+ ).toBe("none")
452
+
453
+ // Every one of them is for the eye. A real `<label>` would name the
454
+ // start control a second time and fight the `id` a `Field` clones
455
+ // onto it, so nothing here is named "Start" or "End".
456
+ for (const caption of [
457
+ narrowStartCaption,
458
+ narrowJoiner,
459
+ narrowEndCaption,
460
+ wideStartCaption,
461
+ wideJoiner,
462
+ wideEndCaption,
463
+ ]) {
464
+ await expect(caption).toHaveAttribute(
465
+ "aria-hidden",
466
+ "true",
467
+ )
468
+ }
469
+
470
+ await expect(
471
+ canvas.queryAllByRole("textbox", {
472
+ name: /^(Start|End)$/,
473
+ }),
474
+ ).toHaveLength(0)
475
+
476
+ await expectNoAxeViolations(canvasElement)
477
+ })
478
+
479
+ test("milliseconds are optional to read as well as to type", async () => {
480
+ const { canvas, single } = await mountInteractive()
481
+
482
+ await userEvent.click(single)
483
+
484
+ // No fraction typed, so no fraction written back. You write 3, not
485
+ // 3.000.
486
+ await userEvent.type(single, "10:00{Enter}")
487
+
488
+ await expect(single).toHaveValue("00:10:00")
489
+
490
+ await waitFor(() => {
491
+ expect(
492
+ canvas.getByText("Chosen: 00:10:00"),
493
+ ).toBeInTheDocument()
494
+ })
495
+
496
+ await userEvent.clear(single)
497
+
498
+ // A real fraction still prints in full, padded on the right.
499
+ await userEvent.type(single, "10:00.25{Enter}")
500
+
501
+ await expect(single).toHaveValue("00:10:00.250")
502
+ })
@@ -64,6 +64,21 @@ const INPUT_CLASS =
64
64
 
65
65
  type Endpoint = "end" | "start"
66
66
 
67
+ /**
68
+ * How this field prints, everywhere it prints — the text it writes
69
+ * back on commit, and the echo that reports what a draft resolves
70
+ * to. The two have to agree, so they read the same constant.
71
+ *
72
+ * `"auto"` rather than the exported default of three digits: a
73
+ * person who types `1:01:00` and tabs away did not ask for
74
+ * `01:01:00.000`, any more than typing `3` asks for `3.000`. A real
75
+ * fraction still prints in full, and the grammar reads both back to
76
+ * the same number.
77
+ */
78
+ const DISPLAY_FORMAT = {
79
+ millisecondDigits: "auto",
80
+ } as const
81
+
67
82
  const toSeededRange = (
68
83
  valueMs: number | TimecodeRange | undefined,
69
84
  ): TimecodeRange =>
@@ -117,6 +132,23 @@ const toSeededRange = (
117
132
  * discard the boundary just typed. A **zero-length** section is
118
133
  * refused by name: it plays nothing, so swapping would hide a typo
119
134
  * behind a window that silently does not exist.
135
+ *
136
+ * ### The Narrow View stacks the two fields
137
+ *
138
+ * Below `--cq-sm` a section is two fields one above the other, under
139
+ * the captions "Start" and "End"; above it, the row with `to`
140
+ * between them. It is a **container** query, so a 390px modal on a
141
+ * 2560px monitor gets the stacked form that a window breakpoint
142
+ * would have denied it. The captions are `aria-hidden`, so the
143
+ * accessible name of each control is the same in both layouts.
144
+ *
145
+ * ### Milliseconds are optional to read as well as to type
146
+ *
147
+ * The grammar has always accepted `1:01:00`. The field now writes it
148
+ * back as `01:01:00` rather than as `01:01:00.000`, through
149
+ * `formatTimecode`'s `millisecondDigits: "auto"`, because a fraction
150
+ * nobody typed is not information. `1:01:00.5` still commits as
151
+ * `01:01:00.500`.
120
152
  */
121
153
  export const TimecodeInput = ({
122
154
  className,
@@ -149,16 +181,29 @@ export const TimecodeInput = ({
149
181
  const toDisplayText = (milliseconds: null | number) =>
150
182
  milliseconds === null
151
183
  ? ""
152
- : formatTimecode(milliseconds)
184
+ : formatTimecode(milliseconds, DISPLAY_FORMAT)
153
185
 
154
186
  const [texts, setTexts] = useState({
155
187
  end: toDisplayText(seededRange.end),
156
188
  start: toDisplayText(seededRange.start),
157
189
  })
158
190
 
191
+ /**
192
+ * Which endpoint the keyboard steps and the echo belong to. It
193
+ * SURVIVES blur, deliberately: a refusal has to keep naming the
194
+ * field it is about after the focus has gone somewhere else.
195
+ */
159
196
  const [activeEndpoint, setActiveEndpoint] =
160
197
  useState<Endpoint>("start")
161
198
 
199
+ /**
200
+ * Whether a field of this control holds the focus. Separate from
201
+ * `activeEndpoint` because the two answer different questions —
202
+ * *which* endpoint, and *whether anybody is typing* — and only the
203
+ * second one may hide the echo.
204
+ */
205
+ const [isFocused, setIsFocused] = useState(false)
206
+
162
207
  /**
163
208
  * A refusal the grammar cannot see, because it is about the
164
209
  * **pair** rather than about either text: a zero-length section.
@@ -355,14 +400,55 @@ export const TimecodeInput = ({
355
400
  : parsed.kind === "empty" || clampedPreview === null
356
401
  ? ""
357
402
  : clampedPreview === parsed.milliseconds
358
- ? formatTimecode(parsed.milliseconds)
359
- : `${formatTimecode(parsed.milliseconds)} is outside the media, so it commits as ${formatTimecode(clampedPreview)}.`
403
+ ? formatTimecode(
404
+ parsed.milliseconds,
405
+ DISPLAY_FORMAT,
406
+ )
407
+ : `${formatTimecode(parsed.milliseconds, DISPLAY_FORMAT)} is outside the media, so it commits as ${formatTimecode(clampedPreview, DISPLAY_FORMAT)}.`
360
408
 
361
- const echoText = rangeRefusal ?? parsedEcho
409
+ /**
410
+ * A restatement is a reading of the field above it, so it leaves
411
+ * with the focus. Left up, an unlabelled `00:05:00` under an idle
412
+ * control reads as a stray value rather than as a report on one.
413
+ *
414
+ * A REFUSAL stays, and both kinds do. `rangeRefusal` is set on
415
+ * commit — which is exactly blur — so a rule that hid it on blur
416
+ * would mean it was never readable at all. And an unparsed text
417
+ * keeps `aria-invalid` on the control, so the sentence saying why
418
+ * has to stay with it: an invalid field with no message is worse
419
+ * than the stray line this removes.
420
+ *
421
+ * Emptying a live region announces nothing, so the blur costs a
422
+ * screen reader no chatter.
423
+ */
424
+ const echoText =
425
+ rangeRefusal ??
426
+ (isFocused || parsed.kind === "unparsed"
427
+ ? parsedEcho
428
+ : "")
362
429
 
363
430
  const isInvalid =
364
431
  rangeRefusal !== null || parsed.kind === "unparsed"
365
432
 
433
+ /**
434
+ * The Narrow View's caption over one field.
435
+ *
436
+ * Presentational, and `aria-hidden` for the same reason the word
437
+ * `to` is: each input already carries
438
+ * `aria-label="<label> <endpoint>"`, and a real `<label>` here
439
+ * would name the start control a second time and fight the `id` a
440
+ * `Field` above clones onto it. The eye gets a caption; the
441
+ * accessible name is the same in both layouts.
442
+ */
443
+ const renderCaption = (text: string) => (
444
+ <span
445
+ aria-hidden="true"
446
+ className="text-content-secondary text-sm cq-sm:hidden"
447
+ >
448
+ {text}
449
+ </span>
450
+ )
451
+
366
452
  const renderInput = (endpoint: Endpoint) => {
367
453
  const ownProps = {
368
454
  "aria-describedby": echoId,
@@ -398,6 +484,8 @@ export const TimecodeInput = ({
398
484
  // rather than by a keyboard the field cannot choose.
399
485
  inputMode: "numeric" as const,
400
486
  onBlur: () => {
487
+ setIsFocused(false)
488
+
401
489
  commitText(endpoint)
402
490
  },
403
491
  onChange: (changeEvent: {
@@ -417,6 +505,8 @@ export const TimecodeInput = ({
417
505
  }))
418
506
  },
419
507
  onFocus: () => {
508
+ setIsFocused(true)
509
+
420
510
  setActiveEndpoint(endpoint)
421
511
  },
422
512
  onKeyDown: (
@@ -444,28 +534,49 @@ export const TimecodeInput = ({
444
534
  }
445
535
 
446
536
  return (
537
+ // `@container` here, not on a wrapper: the box a caller sized is
538
+ // the box that either has room for two timecodes side by side or
539
+ // does not. A viewport breakpoint reads that backwards — a 390px
540
+ // modal on a 2560px monitor is a Wide View by the window and a
541
+ // Narrow View by every measurement that matters. The cost is the
542
+ // `contain: inline-size` that rides along with it, so this field
543
+ // takes its width from its parent rather than from its content:
544
+ // give it a definite inline size, as every container-query
545
+ // component in this library needs.
447
546
  <div
448
547
  className={toClassName(
449
- "flex flex-col gap-1",
548
+ "@container flex flex-col gap-1",
450
549
  className,
451
550
  )}
452
551
  >
453
- <div className="flex items-center gap-2">
454
- {renderInput("start")}
455
-
456
- {isRange ? (
457
- <>
458
- <span
459
- aria-hidden="true"
460
- className="shrink-0 text-content-secondary text-sm"
461
- >
462
- to
463
- </span>
552
+ {isRange ? (
553
+ // Below `--cq-sm` the two fields STACK, each under a caption
554
+ // of its own, because `hh:mm:ss.mmm` twice and the word
555
+ // between them fills a phone-width modal edge to edge with
556
+ // no slack left. Above it, the row is unchanged.
557
+ <div className="flex flex-col gap-2 cq-sm:flex-row cq-sm:items-center cq-sm:gap-2">
558
+ <div className="flex min-w-0 flex-col gap-1 cq-sm:flex-1">
559
+ {renderCaption("Start")}
560
+
561
+ {renderInput("start")}
562
+ </div>
563
+
564
+ <span
565
+ aria-hidden="true"
566
+ className="hidden shrink-0 text-content-secondary text-sm cq-sm:inline"
567
+ >
568
+ to
569
+ </span>
570
+
571
+ <div className="flex min-w-0 flex-col gap-1 cq-sm:flex-1">
572
+ {renderCaption("End")}
464
573
 
465
574
  {renderInput("end")}
466
- </>
467
- ) : null}
468
- </div>
575
+ </div>
576
+ </div>
577
+ ) : (
578
+ renderInput("start")
579
+ )}
469
580
 
470
581
  {/*
471
582
  The anti-silent-guess mechanism, and the reason this field is
@@ -210,6 +210,49 @@ test("millisecondDigits truncates rather than rounding, because a position is no
210
210
  ).toBe("00:00:01")
211
211
  })
212
212
 
213
+ test('millisecondDigits "auto" prints the fraction only when there is one', () => {
214
+ // The complaint this answers: typing `1:01:00` and tabbing away
215
+ // rewrote the field to `01:01:00.000`. You write 3, not 3.000.
216
+ expect(
217
+ formatTimecode(3_660_000, {
218
+ millisecondDigits: "auto",
219
+ }),
220
+ ).toBe("01:01:00")
221
+
222
+ // A real fraction still prints in full, at the same three digits.
223
+ expect(
224
+ formatTimecode(3_660_500, {
225
+ millisecondDigits: "auto",
226
+ }),
227
+ ).toBe("01:01:00.500")
228
+
229
+ expect(
230
+ formatTimecode(1, { millisecondDigits: "auto" }),
231
+ ).toBe("00:00:00.001")
232
+
233
+ expect(
234
+ formatTimecode(0, { millisecondDigits: "auto" }),
235
+ ).toBe("00:00:00")
236
+
237
+ // Both spellings read back to the number that produced them, so
238
+ // the shorter one costs the round trip nothing.
239
+ expect(parsedMilliseconds("01:01:00")).toBe(3_660_000)
240
+ expect(parsedMilliseconds("01:01:00.500")).toBe(3_660_500)
241
+
242
+ // It composes with the other option rather than replacing it.
243
+ expect(
244
+ formatTimecode(90_000, {
245
+ isHoursShown: false,
246
+ millisecondDigits: "auto",
247
+ }),
248
+ ).toBe("01:30")
249
+ })
250
+
251
+ test("the DEFAULT is still three digits, because a published export is read by callers this change never saw", () => {
252
+ expect(formatTimecode(3_660_000)).toBe("01:01:00.000")
253
+ expect(formatTimecode(90_000)).toBe("00:01:30.000")
254
+ })
255
+
213
256
  test("formatTimecode refuses to print a position that does not exist", () => {
214
257
  expect(formatTimecode(-5_000)).toBe("00:00:00.000")
215
258
  expect(formatTimecode(Number.NaN)).toBe("00:00:00.000")
@@ -90,8 +90,17 @@ export type FormatTimecodeOptions = {
90
90
  * unchanged because the largest field present is unbounded.
91
91
  */
92
92
  isHoursShown?: boolean
93
- /** `0` through `3`. `0` drops the fraction and its separator. */
94
- millisecondDigits?: number
93
+ /**
94
+ * `0` through `3`. `0` drops the fraction and its separator.
95
+ *
96
+ * `"auto"` prints the fraction only when there is one to print: a
97
+ * whole number of seconds comes back as `01:01:00`, and half a
98
+ * second more comes back as `01:01:00.500`. It is how a decimal
99
+ * number is written everywhere else — you write `3`, not `3.000` —
100
+ * and the grammar reads both spellings back to the number that
101
+ * produced them, because its fraction is optional.
102
+ */
103
+ millisecondDigits?: "auto" | number
95
104
  }
96
105
 
97
106
  const padTwo = (value: number) =>
@@ -105,12 +114,16 @@ const padTwo = (value: number) =>
105
114
  * because it has not reached second 2 yet. Rounding would print a
106
115
  * seek point the media has not played.
107
116
  *
108
- * The default is the canonical `hh:mm:ss.mmm`, and that is what the
109
- * field writes back on commit. One spelling means a re-read of the
110
- * field parses to the number that produced it, and a screenshot of
111
- * the same value is the same picture — neither of which is true of
112
- * the fleet's five printers, three of which hide the hour and two of
113
- * which do not pad the minute.
117
+ * The default is the canonical `hh:mm:ss.mmm`. `TimecodeInput` asks
118
+ * for `millisecondDigits: "auto"` instead, so a whole second is
119
+ * written back as `01:01:00` rather than as `01:01:00.000` — a
120
+ * person who typed no fraction did not ask for one, and both
121
+ * spellings re-read to the number that produced them.
122
+ *
123
+ * Either way it is ONE spelling per value, which is what the fleet's
124
+ * five printers never were: three of them hide the hour and two do
125
+ * not pad the minute, so the same position is a different picture in
126
+ * each app.
114
127
  */
115
128
  export const formatTimecode = (
116
129
  milliseconds: number,
@@ -123,10 +136,15 @@ export const formatTimecode = (
123
136
  ? Math.max(0, Math.floor(milliseconds))
124
137
  : 0
125
138
 
126
- const digits = Math.min(
127
- 3,
128
- Math.max(0, Math.floor(millisecondDigits)),
129
- )
139
+ const digits =
140
+ millisecondDigits === "auto"
141
+ ? total % MILLISECONDS_PER_SECOND === 0
142
+ ? 0
143
+ : 3
144
+ : Math.min(
145
+ 3,
146
+ Math.max(0, Math.floor(millisecondDigits)),
147
+ )
130
148
 
131
149
  const hours = isHoursShown
132
150
  ? Math.floor(total / MILLISECONDS_PER_HOUR)
@@ -251,6 +251,13 @@ test("a container-query component is never storied in a shrink-to-fit cell", ()
251
251
  // measured in Docket, before any of the steps contained
252
252
  // anything.
253
253
  "Stepper",
254
+ // `TimecodeInput` queries its own box to decide whether a section
255
+ // is `[start] to [end]` in a row or two captioned fields stacked.
256
+ // The width that decides it is a modal's, not a window's: the
257
+ // report came from a 390px dialog, and the same dialog on a
258
+ // 2560px monitor is the Wide View by every media query and the
259
+ // Narrow View by the only measurement that matters.
260
+ "TimecodeInput",
254
261
  ])
255
262
 
256
263
  // `StoryCell`s are never nested, so the lazy match really does