@hjmds/react 1.39.0 → 1.41.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.
@@ -1,17 +1,19 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
- import { timePickerDraft, timePickerItems, resolveTimePickerValue, validateTimePicker, timePickerRecipe } from "@hjmds/design-contracts/components/time-picker";
4
+ import { timePickerDraft, timePickerItems, resolveTimePickerValue, resolveTimePickerWheelIndex, validateTimePicker, timePickerRecipe } from "@hjmds/design-contracts/components/time-picker";
5
5
  import { Button } from "./actions.js";
6
6
  import { Icon } from "./supplemental-display.js";
7
7
  import { Popover } from "./popover.js";
8
+ import { useHjmTheme } from "./provider.js";
8
9
  /** One draft and one confirmation for both columns. Inline mode delegates confirmation to its form. */
9
10
  export function TimePicker(props) {
10
11
  validateTimePicker(props);
11
12
  const { value, onValueChange, labels, disabled, readOnly, description, error, presentation = "field", layoutStyle, className } = props;
12
13
  const [open, setOpen] = useState(false), [draft, setDraft] = useState(() => timePickerDraft(value));
13
14
  const locked = !!(disabled || readOnly);
14
- const panelRef = useRef(null), firstRef = useRef(null);
15
+ const firstRef = useRef(null);
16
+ const [moving, setMoving] = useState({ hour: false, minute: false });
15
17
  const emitted = useRef(null);
16
18
  useEffect(() => {
17
19
  // Preserve an incomplete inline draft when the parent echoes its null validity value.
@@ -25,13 +27,6 @@ export function TimePicker(props) {
25
27
  useEffect(() => { if (locked)
26
28
  setOpen(false); }, [locked]);
27
29
  const resolved = resolveTimePickerValue(draft, props);
28
- useLayoutEffect(() => {
29
- panelRef.current?.querySelectorAll('[role="listbox"]').forEach(list => {
30
- const selected = list.querySelector('[aria-selected="true"]');
31
- if (selected)
32
- list.scrollTop = Math.max(0, selected.offsetTop - (list.clientHeight - selected.offsetHeight) / 2);
33
- });
34
- }, [draft.hour, draft.minute, open]);
35
30
  function select(part, next) {
36
31
  if (locked)
37
32
  return;
@@ -43,32 +38,101 @@ export function TimePicker(props) {
43
38
  onValueChange(result);
44
39
  }
45
40
  }
46
- const panel = _jsxs("div", { style: { "--hjm-time-row": `${timePickerRecipe.rowHeight}px`, "--hjm-time-height": `${timePickerRecipe.rowHeight * timePickerRecipe.visibleRows}px`, "--hjm-time-summary": `${timePickerRecipe.readoutSize}px`, "--hjm-time-summary-line-height": `${timePickerRecipe.readoutLineHeight}px`, maxWidth: timePickerRecipe.panelWidth }, ref: panelRef, className: "hjm-time-picker__panel", role: "group", "aria-label": labels.label, children: [_jsxs("div", { className: "hjm-time-picker__readout", "aria-live": "polite", "aria-atomic": "true", children: [draft.hour ?? "--", _jsx("span", { children: ":" }), draft.minute ?? "--"] }), _jsx("div", { className: "hjm-time-picker__columns", children: ["hour", "minute"].map(part => {
47
- const items = timePickerItems(part, draft, props);
48
- const active = items.find(item => item.value === draft[part] && !item.disabled)?.value ?? items.find(item => !item.disabled)?.value;
49
- return _jsxs("div", { className: "hjm-time-picker__column", children: [_jsx("div", { className: "hjm-time-picker__caption", children: labels[part] }), _jsx("div", { role: "listbox", "aria-label": labels[part], "aria-disabled": locked, className: "hjm-time-picker__list", children: items.map(item => _jsx("button", { ref: part === "hour" && item.value === active ? firstRef : undefined, type: "button", role: "option", "aria-selected": draft[part] === item.value, "aria-label": `${item.value} ${labels[part]}`, disabled: locked || item.disabled, tabIndex: item.value === active ? 0 : -1, onClick: () => select(part, item.value), onFocus: e => {
50
- const list = e.currentTarget.parentElement;
51
- const row = e.currentTarget;
52
- if (row.offsetTop < list.scrollTop)
53
- list.scrollTop = row.offsetTop;
54
- else if (row.offsetTop + row.offsetHeight > list.scrollTop + list.clientHeight)
55
- list.scrollTop = row.offsetTop + row.offsetHeight - list.clientHeight;
56
- }, onKeyDown: e => {
57
- if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(e.key))
58
- return;
59
- e.preventDefault();
60
- const enabled = Array.from(e.currentTarget.parentElement.querySelectorAll('button:not(:disabled)'));
61
- const index = enabled.indexOf(e.currentTarget);
62
- const target = e.key === "Home" ? enabled[0] : e.key === "End" ? enabled.at(-1) : enabled[index + (e.key === "ArrowDown" ? 1 : -1)];
63
- if (target) {
64
- target.focus();
65
- select(part, target.textContent);
66
- }
67
- }, children: item.value }, item.value)) })] }, part);
68
- }) }), presentation === "field" ? _jsx(Button, { disabled: locked || resolved === null, onClick: () => { const current = resolveTimePickerValue(draft, props); if (!locked && current !== null) {
41
+ const panel = _jsxs("div", { style: { "--hjm-time-row": `${timePickerRecipe.rowHeight}px`, "--hjm-time-height": `${timePickerRecipe.rowHeight * timePickerRecipe.visibleRows}px`, "--hjm-time-summary": `${timePickerRecipe.readoutSize}px`, "--hjm-time-summary-line-height": `${timePickerRecipe.readoutLineHeight}px`, maxWidth: timePickerRecipe.panelWidth }, className: "hjm-time-picker__panel", role: "group", "aria-label": labels.label, children: [_jsxs("div", { className: "hjm-time-picker__readout", "aria-live": "polite", "aria-atomic": "true", children: [draft.hour ?? "--", _jsx("span", { children: ":" }), draft.minute ?? "--"] }), _jsx("div", { className: "hjm-time-picker__columns", children: ["hour", "minute"].map(part => _jsx(TimeColumn, { part: part, draft: draft, options: props, locked: locked, firstRef: firstRef, onSelect: next => select(part, next), onMoving: next => setMoving(current => current[part] === next ? current : { ...current, [part]: next }) }, part)) }), presentation === "field" ? _jsx(Button, { disabled: locked || resolved === null || moving.hour || moving.minute, onClick: () => { const current = resolveTimePickerValue(draft, props); if (!locked && current !== null) {
69
42
  onValueChange(current);
70
43
  setOpen(false);
71
44
  } }, children: labels.confirm }) : null] });
72
45
  return _jsxs("div", { className: `hjm-time-picker ${className ?? ""}`, style: layoutStyle, "data-disabled": disabled || undefined, children: [presentation === "inline" ? panel : _jsxs(_Fragment, { children: [_jsx("span", { className: "hjm-time-picker__label", children: labels.label }), _jsx(Popover, { title: labels.label, closeLabel: labels.cancel, closeIcon: _jsx(Icon, { name: "close" }), open: open, onOpenChange: setOpen, initialFocusRef: firstRef, trigger: _jsx("button", { type: "button", className: "hjm-time-picker__trigger", disabled: locked, "aria-label": `${labels.label}, ${value ?? labels.placeholder}`, "aria-invalid": !!error, children: _jsx("span", { children: value ?? labels.placeholder }) }), children: panel })] }), description ? _jsx("span", { className: "hjm-time-picker__description", children: description }) : null, error ? _jsx("span", { role: "alert", className: "hjm-time-picker__error", children: error }) : null] });
73
46
  }
47
+ function TimeColumn({ part, draft, options, locked, firstRef, onSelect, onMoving }) {
48
+ const { environment } = useHjmTheme();
49
+ const wheel = options.selectionStyle === "wheel";
50
+ const listRef = useRef(null);
51
+ const interacting = useRef(false);
52
+ const emitted = useRef(null);
53
+ const items = timePickerItems(part, draft, options);
54
+ const active = items.find(item => item.value === draft[part] && !item.disabled)?.value ?? items.find(item => !item.disabled)?.value;
55
+ const index = items.findIndex(item => item.value === draft[part]);
56
+ const rowHeight = timePickerRecipe.rowHeight;
57
+ const label = options.labels[part];
58
+ useLayoutEffect(() => {
59
+ const list = listRef.current;
60
+ if (!list)
61
+ return;
62
+ // Our own scroll edits must not interrupt momentum when a controlled parent echoes the draft.
63
+ if (wheel && emitted.current === draft[part] && draft[part] !== null) {
64
+ emitted.current = null;
65
+ return;
66
+ }
67
+ interacting.current = false;
68
+ onMoving(false);
69
+ list.scrollTop = wheel ? Math.max(0, index) * rowHeight : Math.max(0, (index - 2) * rowHeight);
70
+ }, [draft[part], wheel, locked, options.minuteStep]);
71
+ function emit(index) {
72
+ const item = items[index];
73
+ if (!item || item.disabled || locked || (wheel && item.value === draft[part]))
74
+ return;
75
+ emitted.current = wheel ? item.value : null;
76
+ onSelect(item.value);
77
+ }
78
+ function choose(index) {
79
+ interacting.current = false;
80
+ onMoving(false);
81
+ emit(index);
82
+ if (wheel) {
83
+ const list = listRef.current;
84
+ if (list?.contains(document.activeElement))
85
+ list.querySelectorAll("button")[index]?.focus({ preventScroll: true });
86
+ list?.scrollTo({ top: index * rowHeight, behavior: environment.reducedMotion ? "instant" : "smooth" });
87
+ }
88
+ }
89
+ useEffect(() => {
90
+ const list = listRef.current;
91
+ if (!wheel || !list)
92
+ return;
93
+ // scrollend includes CSS snapping and waits for touch/trackpad release; no timeout guesses.
94
+ // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollend_event
95
+ const finish = () => {
96
+ if (!interacting.current || locked)
97
+ return;
98
+ interacting.current = false;
99
+ onMoving(false);
100
+ const target = resolveTimePickerWheelIndex(items, list.scrollTop);
101
+ if (target !== null)
102
+ choose(target);
103
+ };
104
+ list.addEventListener("scrollend", finish);
105
+ return () => list.removeEventListener("scrollend", finish);
106
+ });
107
+ return _jsxs("div", { className: "hjm-time-picker__column", children: [_jsx("div", { className: "hjm-time-picker__caption", children: label }), _jsxs("div", { className: "hjm-time-picker__viewport", "data-wheel": wheel || undefined, children: [wheel && _jsx("div", { "aria-hidden": "true", className: "hjm-time-picker__center" }), _jsx("div", { ref: listRef, role: "listbox", "aria-label": label, "aria-disabled": locked, className: "hjm-time-picker__list", "data-wheel": wheel || undefined, onWheel: () => { if (wheel && !locked)
108
+ interacting.current = true; }, onPointerDown: () => { if (wheel && !locked)
109
+ interacting.current = true; }, onScroll: e => {
110
+ if (!wheel || !interacting.current || locked)
111
+ return;
112
+ onMoving(true);
113
+ const target = resolveTimePickerWheelIndex(items, e.currentTarget.scrollTop);
114
+ if (target !== null)
115
+ emit(target);
116
+ }, children: items.map((item, itemIndex) => _jsx("button", { ref: part === "hour" && item.value === active ? firstRef : undefined, type: "button", role: "option", "aria-selected": draft[part] === item.value, "aria-label": `${item.value} ${label}`, disabled: locked || item.disabled, tabIndex: item.value === active ? 0 : -1, onClick: () => choose(itemIndex), onFocus: e => {
117
+ if (wheel)
118
+ return;
119
+ const list = e.currentTarget.parentElement;
120
+ const row = e.currentTarget;
121
+ if (row.offsetTop < list.scrollTop)
122
+ list.scrollTop = row.offsetTop;
123
+ else if (row.offsetTop + row.offsetHeight > list.scrollTop + list.clientHeight)
124
+ list.scrollTop = row.offsetTop + row.offsetHeight - list.clientHeight;
125
+ }, onKeyDown: e => {
126
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(e.key))
127
+ return;
128
+ e.preventDefault();
129
+ const enabled = items.map((entry, index) => ({ ...entry, index })).filter(entry => !entry.disabled);
130
+ const position = enabled.findIndex(entry => entry.value === item.value);
131
+ const target = e.key === "Home" ? enabled[0] : e.key === "End" ? enabled.at(-1) : enabled[position + (e.key === "ArrowDown" ? 1 : -1)];
132
+ if (target) {
133
+ listRef.current?.querySelectorAll("button")[target.index]?.focus({ preventScroll: wheel });
134
+ choose(target.index);
135
+ }
136
+ }, children: item.value }, item.value)) })] })] });
137
+ }
74
138
  //# sourceMappingURL=time-picker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"time-picker.js","sourceRoot":"","sources":["../src/time-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\nimport { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { timePickerDraft, timePickerItems, resolveTimePickerValue, validateTimePicker, timePickerRecipe, type TimePickerOptions, type TimePickerPart } from \"@hjmds/design-contracts/components/time-picker\";\nimport { Button } from \"./actions.js\";\nimport { Icon } from \"./supplemental-display.js\";\nimport { Popover } from \"./popover.js\";\nimport type { HjmCompositionStyleProp } from \"./composition-style.js\";\n\nexport type TimePickerProps = TimePickerOptions & Readonly<{ layoutStyle?: HjmCompositionStyleProp; className?: string }>;\n\n/** One draft and one confirmation for both columns. Inline mode delegates confirmation to its form. */\nexport function TimePicker(props: TimePickerProps) {\n validateTimePicker(props);\n const { value, onValueChange, labels, disabled, readOnly, description, error, presentation = \"field\", layoutStyle, className } = props;\n const [open, setOpen] = useState(false), [draft, setDraft] = useState(() => timePickerDraft(value));\n const locked = !!(disabled || readOnly);\n const panelRef = useRef<HTMLDivElement>(null), firstRef = useRef<HTMLButtonElement>(null);\n const emitted = useRef<{ value: string | null } | null>(null);\n useEffect(() => {\n // Preserve an incomplete inline draft when the parent echoes its null validity value.\n if (presentation === \"inline\" && emitted.current?.value === value) { emitted.current = null; return; }\n emitted.current = null; setDraft(timePickerDraft(value));\n }, [value, open, presentation]);\n useEffect(() => { if (locked) setOpen(false); }, [locked]);\n const resolved = resolveTimePickerValue(draft, props);\n useLayoutEffect(() => {\n panelRef.current?.querySelectorAll<HTMLElement>('[role=\"listbox\"]').forEach(list => {\n const selected = list.querySelector<HTMLElement>('[aria-selected=\"true\"]');\n if (selected) list.scrollTop = Math.max(0, selected.offsetTop - (list.clientHeight - selected.offsetHeight) / 2);\n });\n }, [draft.hour, draft.minute, open]);\n function select(part: TimePickerPart, next: string) {\n if (locked) return;\n const updated = { ...draft, [part]: next }; setDraft(updated);\n const result = resolveTimePickerValue(updated, props);\n if (presentation === \"inline\") { emitted.current = { value: result }; onValueChange(result); }\n }\n const panel = <div style={{ \"--hjm-time-row\": `${timePickerRecipe.rowHeight}px`, \"--hjm-time-height\": `${timePickerRecipe.rowHeight * timePickerRecipe.visibleRows}px`, \"--hjm-time-summary\": `${timePickerRecipe.readoutSize}px`, \"--hjm-time-summary-line-height\": `${timePickerRecipe.readoutLineHeight}px`, maxWidth: timePickerRecipe.panelWidth } as CSSProperties} ref={panelRef} className=\"hjm-time-picker__panel\" role=\"group\" aria-label={labels.label}>\n <div className=\"hjm-time-picker__readout\" aria-live=\"polite\" aria-atomic=\"true\">{draft.hour ?? \"--\"}<span>:</span>{draft.minute ?? \"--\"}</div>\n <div className=\"hjm-time-picker__columns\">\n {([\"hour\", \"minute\"] as const).map(part => {\n const items = timePickerItems(part, draft, props);\n const active = items.find(item => item.value === draft[part] && !item.disabled)?.value ?? items.find(item => !item.disabled)?.value;\n return <div key={part} className=\"hjm-time-picker__column\"><div className=\"hjm-time-picker__caption\">{labels[part]}</div>\n <div role=\"listbox\" aria-label={labels[part]} aria-disabled={locked} className=\"hjm-time-picker__list\">\n {items.map(item => <button key={item.value} ref={part === \"hour\" && item.value === active ? firstRef : undefined} type=\"button\" role=\"option\"\n aria-selected={draft[part] === item.value} aria-label={`${item.value} ${labels[part]}`} disabled={locked || item.disabled} tabIndex={item.value === active ? 0 : -1}\n onClick={() => select(part, item.value)} onFocus={e => {\n const list = e.currentTarget.parentElement!; const row = e.currentTarget;\n if (row.offsetTop < list.scrollTop) list.scrollTop = row.offsetTop;\n else if (row.offsetTop + row.offsetHeight > list.scrollTop + list.clientHeight) list.scrollTop = row.offsetTop + row.offsetHeight - list.clientHeight;\n }}\n onKeyDown={e => {\n if (![\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(e.key)) return;\n e.preventDefault();\n const enabled = Array.from(e.currentTarget.parentElement!.querySelectorAll<HTMLButtonElement>('button:not(:disabled)'));\n const index = enabled.indexOf(e.currentTarget);\n const target = e.key === \"Home\" ? enabled[0] : e.key === \"End\" ? enabled.at(-1) : enabled[index + (e.key === \"ArrowDown\" ? 1 : -1)];\n if (target) { target.focus(); select(part, target.textContent!); }\n }}>{item.value}</button>)}\n </div></div>;\n })}\n </div>\n {presentation === \"field\" ? <Button disabled={locked || resolved === null} onClick={() => { const current = resolveTimePickerValue(draft, props); if (!locked && current !== null) { onValueChange(current); setOpen(false); } }}>{labels.confirm}</Button> : null}\n </div>;\n return <div className={`hjm-time-picker ${className ?? \"\"}`} style={layoutStyle} data-disabled={disabled || undefined}>\n {presentation === \"inline\" ? panel : <><span className=\"hjm-time-picker__label\">{labels.label}</span><Popover title={labels.label} closeLabel={labels.cancel} closeIcon={<Icon name=\"close\" />} open={open} onOpenChange={setOpen} initialFocusRef={firstRef}\n trigger={<button type=\"button\" className=\"hjm-time-picker__trigger\" disabled={locked} aria-label={`${labels.label}, ${value ?? labels.placeholder}`} aria-invalid={!!error}><span>{value ?? labels.placeholder}</span></button>}>{panel}</Popover></>}\n {description ? <span className=\"hjm-time-picker__description\">{description}</span> : null}\n {error ? <span role=\"alert\" className=\"hjm-time-picker__error\">{error}</span> : null}\n </div>;\n}\n"]}
1
+ {"version":3,"file":"time-picker.js","sourceRoot":"","sources":["../src/time-picker.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AACb,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAsC,MAAM,OAAO,CAAC;AACzG,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,gBAAgB,EAA+C,MAAM,gDAAgD,CAAC;AAC1O,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAI5C,uGAAuG;AACvG,MAAM,UAAU,UAAU,CAAC,KAAsB;IAC/C,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC1B,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IACvI,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;IACpG,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,QAAQ,IAAI,QAAQ,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IACjD,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACrE,MAAM,OAAO,GAAG,MAAM,CAAkC,IAAI,CAAC,CAAC;IAC9D,SAAS,CAAC,GAAG,EAAE;QACb,sFAAsF;QACtF,IAAI,YAAY,KAAK,QAAQ,IAAI,OAAO,CAAC,OAAO,EAAE,KAAK,KAAK,KAAK,EAAE,CAAC;YAAC,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;YAAC,OAAO;QAAC,CAAC;QACtG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3D,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC;IAChC,SAAS,CAAC,GAAG,EAAE,GAAG,IAAI,MAAM;QAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3D,MAAM,QAAQ,GAAG,sBAAsB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACtD,SAAS,MAAM,CAAC,IAAoB,EAAE,IAAY;QAChD,IAAI,MAAM;YAAE,OAAO;QACnB,MAAM,OAAO,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC;QAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC9D,MAAM,MAAM,GAAG,sBAAsB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtD,IAAI,YAAY,KAAK,QAAQ,EAAE,CAAC;YAAC,OAAO,CAAC,OAAO,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;YAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAAC,CAAC;IAChG,CAAC;IACD,MAAM,KAAK,GAAG,eAAK,KAAK,EAAE,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAAC,SAAS,IAAI,EAAE,mBAAmB,EAAE,GAAG,gBAAgB,CAAC,SAAS,GAAG,gBAAgB,CAAC,WAAW,IAAI,EAAE,oBAAoB,EAAE,GAAG,gBAAgB,CAAC,WAAW,IAAI,EAAE,gCAAgC,EAAE,GAAG,gBAAgB,CAAC,iBAAiB,IAAI,EAAE,QAAQ,EAAE,gBAAgB,CAAC,UAAU,EAAmB,EAAE,SAAS,EAAC,wBAAwB,EAAC,IAAI,EAAC,OAAO,gBAAa,MAAM,CAAC,KAAK,aAChb,eAAK,SAAS,EAAC,0BAA0B,eAAW,QAAQ,iBAAa,MAAM,aAAE,KAAK,CAAC,IAAI,IAAI,IAAI,EAAC,+BAAc,EAAC,KAAK,CAAC,MAAM,IAAI,IAAI,IAAO,EAC9I,cAAK,SAAS,EAAC,0BAA0B,YACrC,CAAC,MAAM,EAAE,QAAQ,CAAW,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,KAAC,UAAU,IAAY,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAC5I,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,IADnF,IAAI,CACmF,CAAC,GAChJ,EACL,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,KAAC,MAAM,IAAC,QAAQ,EAAE,MAAM,IAAI,QAAQ,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,MAAM,OAAO,GAAG,sBAAsB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;oBAAC,aAAa,CAAC,OAAO,CAAC,CAAC;oBAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAAC,CAAC,CAAC,CAAC,YAAG,MAAM,CAAC,OAAO,GAAU,CAAC,CAAC,CAAC,IAAI,IAC9R,CAAC;IACP,OAAO,eAAK,SAAS,EAAE,mBAAmB,SAAS,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,mBAAiB,QAAQ,IAAI,SAAS,aAClH,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,8BAAE,eAAM,SAAS,EAAC,wBAAwB,YAAE,MAAM,CAAC,KAAK,GAAQ,EAAA,KAAC,OAAO,IAAC,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,QAAQ,EAC1P,OAAO,EAAE,iBAAQ,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,0BAA0B,EAAC,QAAQ,EAAE,MAAM,gBAAc,GAAG,MAAM,CAAC,KAAK,KAAK,KAAK,IAAI,MAAM,CAAC,WAAW,EAAE,kBAAgB,CAAC,CAAC,KAAK,YAAE,yBAAO,KAAK,IAAI,MAAM,CAAC,WAAW,GAAQ,GAAS,YAAG,KAAK,GAAW,IAAG,EACtP,WAAW,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,8BAA8B,YAAE,WAAW,GAAQ,CAAC,CAAC,CAAC,IAAI,EACxF,KAAK,CAAC,CAAC,CAAC,eAAM,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,wBAAwB,YAAE,KAAK,GAAQ,CAAC,CAAC,CAAC,IAAI,IAChF,CAAC;AACT,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAG/E;IACC,MAAM,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,CAAC;IACtC,MAAM,KAAK,GAAG,OAAO,CAAC,cAAc,KAAK,OAAO,CAAC;IACjD,MAAM,OAAO,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IAC7C,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC;IACpI,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG,gBAAgB,CAAC,SAAS,CAAC;IAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEnC,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,8FAA8F;QAC9F,IAAI,KAAK,IAAI,OAAO,CAAC,OAAO,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAAC,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;YAAC,OAAO;QAAC,CAAC;QACzG,WAAW,CAAC,OAAO,GAAG,KAAK,CAAC;QAC5B,QAAQ,CAAC,KAAK,CAAC,CAAC;QAChB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACjG,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;IAErD,SAAS,IAAI,CAAC,KAAa;QACzB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,IAAI,MAAM,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC;YAAE,OAAO;QACtF,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;QAC5C,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IACD,SAAS,MAAM,CAAC,KAAa;QAC3B,WAAW,CAAC,OAAO,GAAG,KAAK,CAAC;QAC5B,QAAQ,CAAC,KAAK,CAAC,CAAC;QAChB,IAAI,CAAC,KAAK,CAAC,CAAC;QACZ,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;YAC7B,IAAI,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC;gBAAE,IAAI,CAAC,gBAAgB,CAAoB,QAAQ,CAAC,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YACtI,IAAI,EAAE,QAAQ,CAAC,EAAE,GAAG,EAAE,KAAK,GAAG,SAAS,EAAE,QAAQ,EAAE,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QACzG,CAAC;IACH,CAAC;IACD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI;YAAE,OAAO;QAC5B,4FAA4F;QAC5F,2EAA2E;QAC3E,MAAM,MAAM,GAAG,GAAG,EAAE;YAClB,IAAI,CAAC,WAAW,CAAC,OAAO,IAAI,MAAM;gBAAE,OAAO;YAC3C,WAAW,CAAC,OAAO,GAAG,KAAK,CAAC;YAC5B,QAAQ,CAAC,KAAK,CAAC,CAAC;YAChB,MAAM,MAAM,GAAG,2BAA2B,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAClE,IAAI,MAAM,KAAK,IAAI;gBAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACtC,CAAC,CAAC;QACF,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC3C,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,OAAO,eAAK,SAAS,EAAC,yBAAyB,aAC7C,cAAK,SAAS,EAAC,0BAA0B,YAAE,KAAK,GAAO,EACvD,eAAK,SAAS,EAAC,2BAA2B,gBAAa,KAAK,IAAI,SAAS,aACtE,KAAK,IAAI,6BAAiB,MAAM,EAAC,SAAS,EAAC,yBAAyB,GAAG,EACxE,cAAK,GAAG,EAAE,OAAO,EAAE,IAAI,EAAC,SAAS,gBAAa,KAAK,mBAAiB,MAAM,EAAE,SAAS,EAAC,uBAAuB,gBAAa,KAAK,IAAI,SAAS,EAC1I,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,KAAK,IAAI,CAAC,MAAM;4BAAE,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,EACpE,aAAa,EAAE,GAAG,EAAE,GAAG,IAAI,KAAK,IAAI,CAAC,MAAM;4BAAE,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,EAC1E,QAAQ,EAAE,CAAC,CAAC,EAAE;4BACZ,IAAI,CAAC,KAAK,IAAI,CAAC,WAAW,CAAC,OAAO,IAAI,MAAM;gCAAE,OAAO;4BACrD,QAAQ,CAAC,IAAI,CAAC,CAAC;4BACf,MAAM,MAAM,GAAG,2BAA2B,CAAC,KAAK,EAAE,CAAC,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;4BAC7E,IAAI,MAAM,KAAK,IAAI;gCAAE,IAAI,CAAC,MAAM,CAAC,CAAC;wBACpC,CAAC,YACA,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,CAAC,iBAAyB,GAAG,EAAE,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,EAAC,QAAQ,EAAC,IAAI,EAAC,QAAQ,mBACzI,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,gBAAc,GAAG,IAAI,CAAC,KAAK,IAAI,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAC5J,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE;gCAC7C,IAAI,KAAK;oCAAE,OAAO;gCAClB,MAAM,IAAI,GAAG,CAAC,CAAC,aAAa,CAAC,aAAc,CAAC;gCAAC,MAAM,GAAG,GAAG,CAAC,CAAC,aAAa,CAAC;gCACzE,IAAI,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS;oCAAE,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;qCAC9D,IAAI,GAAG,CAAC,SAAS,GAAG,GAAG,CAAC,YAAY,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,YAAY;oCAAE,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,GAAG,GAAG,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;4BACxJ,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE;gCAChB,IAAI,CAAC,CAAC,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC;oCAAE,OAAO;gCACrE,CAAC,CAAC,cAAc,EAAE,CAAC;gCACnB,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gCACpG,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC;gCACxE,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gCACvI,IAAI,MAAM,EAAE,CAAC;oCACX,OAAO,CAAC,OAAO,EAAE,gBAAgB,CAAoB,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;oCAC9G,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gCACvB,CAAC;4BACH,CAAC,YAAG,IAAI,CAAC,KAAK,IAjB6B,IAAI,CAAC,KAAK,CAiB7B,CAAC,GACvB,IACF,IACF,CAAC;AACT,CAAC","sourcesContent":["\"use client\";\nimport { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type RefObject } from \"react\";\nimport { timePickerDraft, timePickerItems, resolveTimePickerValue, resolveTimePickerWheelIndex, validateTimePicker, timePickerRecipe, type TimePickerOptions, type TimePickerPart } from \"@hjmds/design-contracts/components/time-picker\";\nimport { Button } from \"./actions.js\";\nimport { Icon } from \"./supplemental-display.js\";\nimport { Popover } from \"./popover.js\";\nimport type { HjmCompositionStyleProp } from \"./composition-style.js\";\nimport { useHjmTheme } from \"./provider.js\";\n\nexport type TimePickerProps = TimePickerOptions & Readonly<{ layoutStyle?: HjmCompositionStyleProp; className?: string }>;\n\n/** One draft and one confirmation for both columns. Inline mode delegates confirmation to its form. */\nexport function TimePicker(props: TimePickerProps) {\n validateTimePicker(props);\n const { value, onValueChange, labels, disabled, readOnly, description, error, presentation = \"field\", layoutStyle, className } = props;\n const [open, setOpen] = useState(false), [draft, setDraft] = useState(() => timePickerDraft(value));\n const locked = !!(disabled || readOnly);\n const firstRef = useRef<HTMLButtonElement>(null);\n const [moving, setMoving] = useState({ hour: false, minute: false });\n const emitted = useRef<{ value: string | null } | null>(null);\n useEffect(() => {\n // Preserve an incomplete inline draft when the parent echoes its null validity value.\n if (presentation === \"inline\" && emitted.current?.value === value) { emitted.current = null; return; }\n emitted.current = null; setDraft(timePickerDraft(value));\n }, [value, open, presentation]);\n useEffect(() => { if (locked) setOpen(false); }, [locked]);\n const resolved = resolveTimePickerValue(draft, props);\n function select(part: TimePickerPart, next: string) {\n if (locked) return;\n const updated = { ...draft, [part]: next }; setDraft(updated);\n const result = resolveTimePickerValue(updated, props);\n if (presentation === \"inline\") { emitted.current = { value: result }; onValueChange(result); }\n }\n const panel = <div style={{ \"--hjm-time-row\": `${timePickerRecipe.rowHeight}px`, \"--hjm-time-height\": `${timePickerRecipe.rowHeight * timePickerRecipe.visibleRows}px`, \"--hjm-time-summary\": `${timePickerRecipe.readoutSize}px`, \"--hjm-time-summary-line-height\": `${timePickerRecipe.readoutLineHeight}px`, maxWidth: timePickerRecipe.panelWidth } as CSSProperties} className=\"hjm-time-picker__panel\" role=\"group\" aria-label={labels.label}>\n <div className=\"hjm-time-picker__readout\" aria-live=\"polite\" aria-atomic=\"true\">{draft.hour ?? \"--\"}<span>:</span>{draft.minute ?? \"--\"}</div>\n <div className=\"hjm-time-picker__columns\">\n {([\"hour\", \"minute\"] as const).map(part => <TimeColumn key={part} part={part} draft={draft} options={props} locked={locked} firstRef={firstRef}\n onSelect={next => select(part, next)} onMoving={next => setMoving(current => current[part] === next ? current : { ...current, [part]: next })} />)}\n </div>\n {presentation === \"field\" ? <Button disabled={locked || resolved === null || moving.hour || moving.minute} onClick={() => { const current = resolveTimePickerValue(draft, props); if (!locked && current !== null) { onValueChange(current); setOpen(false); } }}>{labels.confirm}</Button> : null}\n </div>;\n return <div className={`hjm-time-picker ${className ?? \"\"}`} style={layoutStyle} data-disabled={disabled || undefined}>\n {presentation === \"inline\" ? panel : <><span className=\"hjm-time-picker__label\">{labels.label}</span><Popover title={labels.label} closeLabel={labels.cancel} closeIcon={<Icon name=\"close\" />} open={open} onOpenChange={setOpen} initialFocusRef={firstRef}\n trigger={<button type=\"button\" className=\"hjm-time-picker__trigger\" disabled={locked} aria-label={`${labels.label}, ${value ?? labels.placeholder}`} aria-invalid={!!error}><span>{value ?? labels.placeholder}</span></button>}>{panel}</Popover></>}\n {description ? <span className=\"hjm-time-picker__description\">{description}</span> : null}\n {error ? <span role=\"alert\" className=\"hjm-time-picker__error\">{error}</span> : null}\n </div>;\n}\n\nfunction TimeColumn({ part, draft, options, locked, firstRef, onSelect, onMoving }: {\n part: TimePickerPart; draft: ReturnType<typeof timePickerDraft>; options: TimePickerOptions; locked: boolean;\n firstRef: RefObject<HTMLButtonElement | null>; onSelect(value: string): void; onMoving(moving: boolean): void;\n}) {\n const { environment } = useHjmTheme();\n const wheel = options.selectionStyle === \"wheel\";\n const listRef = useRef<HTMLDivElement>(null);\n const interacting = useRef(false);\n const emitted = useRef<string | null>(null);\n const items = timePickerItems(part, draft, options);\n const active = items.find(item => item.value === draft[part] && !item.disabled)?.value ?? items.find(item => !item.disabled)?.value;\n const index = items.findIndex(item => item.value === draft[part]);\n const rowHeight = timePickerRecipe.rowHeight;\n const label = options.labels[part];\n\n useLayoutEffect(() => {\n const list = listRef.current;\n if (!list) return;\n // Our own scroll edits must not interrupt momentum when a controlled parent echoes the draft.\n if (wheel && emitted.current === draft[part] && draft[part] !== null) { emitted.current = null; return; }\n interacting.current = false;\n onMoving(false);\n list.scrollTop = wheel ? Math.max(0, index) * rowHeight : Math.max(0, (index - 2) * rowHeight);\n }, [draft[part], wheel, locked, options.minuteStep]);\n\n function emit(index: number) {\n const item = items[index];\n if (!item || item.disabled || locked || (wheel && item.value === draft[part])) return;\n emitted.current = wheel ? item.value : null;\n onSelect(item.value);\n }\n function choose(index: number) {\n interacting.current = false;\n onMoving(false);\n emit(index);\n if (wheel) {\n const list = listRef.current;\n if (list?.contains(document.activeElement)) list.querySelectorAll<HTMLButtonElement>(\"button\")[index]?.focus({ preventScroll: true });\n list?.scrollTo({ top: index * rowHeight, behavior: environment.reducedMotion ? \"instant\" : \"smooth\" });\n }\n }\n useEffect(() => {\n const list = listRef.current;\n if (!wheel || !list) return;\n // scrollend includes CSS snapping and waits for touch/trackpad release; no timeout guesses.\n // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollend_event\n const finish = () => {\n if (!interacting.current || locked) return;\n interacting.current = false;\n onMoving(false);\n const target = resolveTimePickerWheelIndex(items, list.scrollTop);\n if (target !== null) choose(target);\n };\n list.addEventListener(\"scrollend\", finish);\n return () => list.removeEventListener(\"scrollend\", finish);\n });\n\n return <div className=\"hjm-time-picker__column\">\n <div className=\"hjm-time-picker__caption\">{label}</div>\n <div className=\"hjm-time-picker__viewport\" data-wheel={wheel || undefined}>\n {wheel && <div aria-hidden=\"true\" className=\"hjm-time-picker__center\" />}\n <div ref={listRef} role=\"listbox\" aria-label={label} aria-disabled={locked} className=\"hjm-time-picker__list\" data-wheel={wheel || undefined}\n onWheel={() => { if (wheel && !locked) interacting.current = true; }}\n onPointerDown={() => { if (wheel && !locked) interacting.current = true; }}\n onScroll={e => {\n if (!wheel || !interacting.current || locked) return;\n onMoving(true);\n const target = resolveTimePickerWheelIndex(items, e.currentTarget.scrollTop);\n if (target !== null) emit(target);\n }}>\n {items.map((item, itemIndex) => <button key={item.value} ref={part === \"hour\" && item.value === active ? firstRef : undefined} type=\"button\" role=\"option\"\n aria-selected={draft[part] === item.value} aria-label={`${item.value} ${label}`} disabled={locked || item.disabled} tabIndex={item.value === active ? 0 : -1}\n onClick={() => choose(itemIndex)} onFocus={e => {\n if (wheel) return;\n const list = e.currentTarget.parentElement!; const row = e.currentTarget;\n if (row.offsetTop < list.scrollTop) list.scrollTop = row.offsetTop;\n else if (row.offsetTop + row.offsetHeight > list.scrollTop + list.clientHeight) list.scrollTop = row.offsetTop + row.offsetHeight - list.clientHeight;\n }} onKeyDown={e => {\n if (![\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(e.key)) return;\n e.preventDefault();\n const enabled = items.map((entry, index) => ({ ...entry, index })).filter(entry => !entry.disabled);\n const position = enabled.findIndex(entry => entry.value === item.value);\n const target = e.key === \"Home\" ? enabled[0] : e.key === \"End\" ? enabled.at(-1) : enabled[position + (e.key === \"ArrowDown\" ? 1 : -1)];\n if (target) {\n listRef.current?.querySelectorAll<HTMLButtonElement>(\"button\")[target.index]?.focus({ preventScroll: wheel });\n choose(target.index);\n }\n }}>{item.value}</button>)}\n </div>\n </div>\n </div>;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hjmds/react",
3
- "version": "1.39.0",
3
+ "version": "1.41.0",
4
4
  "description": "Accessible React 19 renderers for HJM design contracts.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -371,6 +371,11 @@
371
371
  "import": "./dist/rating.js",
372
372
  "default": "./dist/rating.js"
373
373
  },
374
+ "./choice-row": {
375
+ "types": "./dist/choice-row.d.ts",
376
+ "import": "./dist/choice-row.js",
377
+ "default": "./dist/choice-row.js"
378
+ },
374
379
  "./image-comparison": {
375
380
  "types": "./dist/image-comparison.d.ts",
376
381
  "import": "./dist/image-comparison.js",
@@ -598,6 +603,11 @@
598
603
  "types": "./dist/time-picker.d.ts",
599
604
  "import": "./dist/time-picker.js",
600
605
  "default": "./dist/time-picker.js"
606
+ },
607
+ "./image-viewer": {
608
+ "types": "./dist/image-viewer.d.ts",
609
+ "import": "./dist/image-viewer.js",
610
+ "default": "./dist/image-viewer.js"
601
611
  }
602
612
  },
603
613
  "files": [
@@ -608,7 +618,7 @@
608
618
  "THIRD_PARTY_NOTICES.md"
609
619
  ],
610
620
  "peerDependencies": {
611
- "@hjmds/design-contracts": ">=1.39.0 <1.40.0",
621
+ "@hjmds/design-contracts": ">=1.41.0 <1.42.0",
612
622
  "react": ">=19.0.0",
613
623
  "react-dom": ">=19.0.0",
614
624
  "@number-flow/react": "0.6.2",
@@ -646,7 +656,7 @@
646
656
  "@blobatar/react": "2.7.0",
647
657
  "lucide-react": "1.59.0",
648
658
  "vite": "8.3.4",
649
- "@hjmds/design-contracts": "1.39.0"
659
+ "@hjmds/design-contracts": "1.41.0"
650
660
  },
651
661
  "peerDependenciesMeta": {
652
662
  "@number-flow/react": {