scrubtime 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +73 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +73 -24
- package/dist/index.js.map +1 -1
- package/dist/styles.css +39 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -41,8 +41,12 @@ function DraggableValue({
|
|
|
41
41
|
}) {
|
|
42
42
|
const [isEditing, setIsEditing] = (0, import_react.useState)(false);
|
|
43
43
|
const [editValue, setEditValue] = (0, import_react.useState)("");
|
|
44
|
+
const [showMagnifier, setShowMagnifier] = (0, import_react.useState)(false);
|
|
44
45
|
const isDragging = (0, import_react.useRef)(false);
|
|
45
46
|
const hasDragged = (0, import_react.useRef)(false);
|
|
47
|
+
const isLongPress = (0, import_react.useRef)(false);
|
|
48
|
+
const needsBuffer = (0, import_react.useRef)(false);
|
|
49
|
+
const longPressTimer = (0, import_react.useRef)(null);
|
|
46
50
|
const startX = (0, import_react.useRef)(0);
|
|
47
51
|
const startY = (0, import_react.useRef)(0);
|
|
48
52
|
const lastX = (0, import_react.useRef)(0);
|
|
@@ -94,34 +98,76 @@ function DraggableValue({
|
|
|
94
98
|
const touch = e.touches[0];
|
|
95
99
|
isDragging.current = true;
|
|
96
100
|
hasDragged.current = false;
|
|
101
|
+
isLongPress.current = false;
|
|
97
102
|
startX.current = touch.clientX;
|
|
98
103
|
startY.current = touch.clientY;
|
|
99
104
|
lastX.current = touch.clientX;
|
|
100
105
|
lastY.current = touch.clientY;
|
|
101
106
|
accumulatedDelta.current = 0;
|
|
107
|
+
longPressTimer.current = setTimeout(() => {
|
|
108
|
+
isLongPress.current = true;
|
|
109
|
+
setShowMagnifier(true);
|
|
110
|
+
}, 300);
|
|
102
111
|
const handleTouchMove = (e2) => {
|
|
103
112
|
if (!isDragging.current) return;
|
|
104
113
|
const touch2 = e2.touches[0];
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
if (!hasDragged.current) {
|
|
115
|
+
const totalDeltaX = touch2.clientX - startX.current;
|
|
116
|
+
const totalDeltaY = touch2.clientY - startY.current;
|
|
117
|
+
if (isLongPress.current) {
|
|
118
|
+
const deltaX2 = touch2.clientX - lastX.current;
|
|
119
|
+
const deltaY2 = touch2.clientY - lastY.current;
|
|
120
|
+
if (Math.abs(deltaX2) > 0 || Math.abs(deltaY2) > 0) {
|
|
121
|
+
hasDragged.current = true;
|
|
122
|
+
e2.preventDefault();
|
|
123
|
+
const direction = deltaX2 + deltaY2 > 0 ? 1 : -1;
|
|
124
|
+
onDeltaRef.current(direction);
|
|
125
|
+
lastX.current = touch2.clientX;
|
|
126
|
+
lastY.current = touch2.clientY;
|
|
127
|
+
accumulatedDelta.current = -direction * 0.8;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (Math.abs(totalDeltaX) > DRAG_THRESHOLD || Math.abs(totalDeltaY) > DRAG_THRESHOLD) {
|
|
132
|
+
if (longPressTimer.current) {
|
|
133
|
+
clearTimeout(longPressTimer.current);
|
|
134
|
+
longPressTimer.current = null;
|
|
135
|
+
}
|
|
136
|
+
hasDragged.current = true;
|
|
137
|
+
needsBuffer.current = true;
|
|
138
|
+
setShowMagnifier(true);
|
|
139
|
+
e2.preventDefault();
|
|
140
|
+
lastX.current = touch2.clientX;
|
|
141
|
+
lastY.current = touch2.clientY;
|
|
142
|
+
accumulatedDelta.current = 0;
|
|
143
|
+
}
|
|
144
|
+
lastX.current = touch2.clientX;
|
|
145
|
+
lastY.current = touch2.clientY;
|
|
146
|
+
return;
|
|
110
147
|
}
|
|
148
|
+
e2.preventDefault();
|
|
111
149
|
const deltaX = touch2.clientX - lastX.current;
|
|
112
150
|
const deltaY = touch2.clientY - lastY.current;
|
|
113
|
-
const deltaValue = (deltaX
|
|
151
|
+
const deltaValue = (deltaX + deltaY) / sensitivity;
|
|
114
152
|
accumulatedDelta.current += deltaValue;
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
153
|
+
const threshold = needsBuffer.current ? 2 : 1;
|
|
154
|
+
if (Math.abs(accumulatedDelta.current) >= threshold) {
|
|
155
|
+
const wholeDelta = Math.trunc(accumulatedDelta.current);
|
|
117
156
|
accumulatedDelta.current -= wholeDelta;
|
|
118
157
|
onDeltaRef.current(wholeDelta);
|
|
158
|
+
needsBuffer.current = false;
|
|
119
159
|
}
|
|
120
160
|
lastX.current = touch2.clientX;
|
|
121
161
|
lastY.current = touch2.clientY;
|
|
122
162
|
};
|
|
123
163
|
const handleTouchEnd = () => {
|
|
124
164
|
isDragging.current = false;
|
|
165
|
+
needsBuffer.current = false;
|
|
166
|
+
if (longPressTimer.current) {
|
|
167
|
+
clearTimeout(longPressTimer.current);
|
|
168
|
+
longPressTimer.current = null;
|
|
169
|
+
}
|
|
170
|
+
setShowMagnifier(false);
|
|
125
171
|
document.removeEventListener("touchmove", handleTouchMove);
|
|
126
172
|
document.removeEventListener("touchend", handleTouchEnd);
|
|
127
173
|
};
|
|
@@ -181,21 +227,24 @@ function DraggableValue({
|
|
|
181
227
|
}
|
|
182
228
|
);
|
|
183
229
|
}
|
|
184
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.
|
|
185
|
-
"div",
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
230
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "scrubtime-value-wrapper", children: [
|
|
231
|
+
showMagnifier && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "scrubtime-magnifier", "aria-hidden": "true", children: displayValue }),
|
|
232
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
233
|
+
"div",
|
|
234
|
+
{
|
|
235
|
+
onMouseDown: handleMouseDown,
|
|
236
|
+
onTouchStart: handleTouchStart,
|
|
237
|
+
onClick: handleClick,
|
|
238
|
+
onKeyDown: handleDivKeyDown,
|
|
239
|
+
className: `scrubtime-value ${className || ""} ${disabled ? "scrubtime-value--disabled" : ""}`,
|
|
240
|
+
role: "spinbutton",
|
|
241
|
+
"aria-valuenow": value,
|
|
242
|
+
"aria-disabled": disabled,
|
|
243
|
+
tabIndex: disabled ? -1 : 0,
|
|
244
|
+
children: displayValue
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
] });
|
|
199
248
|
}
|
|
200
249
|
function parseTime(time) {
|
|
201
250
|
const [h, m] = time.split(":").map(Number);
|
|
@@ -281,7 +330,7 @@ function TimePicker({
|
|
|
281
330
|
onSet: handleMinutesSet,
|
|
282
331
|
formatValue: (v) => String(v).padStart(2, "0"),
|
|
283
332
|
disabled,
|
|
284
|
-
sensitivity: dragSensitivity
|
|
333
|
+
sensitivity: dragSensitivity,
|
|
285
334
|
min: 0,
|
|
286
335
|
max: 59,
|
|
287
336
|
className: "scrubtime-minutes"
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/TimePicker.tsx"],"sourcesContent":["export { TimePicker } from './TimePicker';\nexport type { TimePickerProps } from './TimePicker';\n","import { useRef, useCallback, useState } from 'react';\n\nexport interface TimePickerProps {\n /** Current time value in \"H:mm\" or \"HH:mm\" format */\n value: string;\n /** Callback fired when time changes */\n onChange: (value: string) => void;\n /** Optional label displayed above the picker */\n label?: string;\n /** Custom class name for the root element */\n className?: string;\n /** Disable the picker */\n disabled?: boolean;\n /** Slider step in minutes (default: 15) */\n sliderStep?: number;\n /** Drag sensitivity - pixels per unit change (default: 3) */\n dragSensitivity?: number;\n /** Number of equal parts to divide the 24h range into (default: 4 = labels at 0,6,12,18,24) */\n divisions?: number;\n}\n\ninterface DraggableValueProps {\n value: number;\n onDelta: (delta: number) => void;\n onSet: (value: number) => void;\n formatValue?: (v: number) => string;\n className?: string;\n disabled?: boolean;\n sensitivity: number;\n min: number;\n max: number;\n}\n\nconst DRAG_THRESHOLD = 3;\n\nfunction DraggableValue({\n value,\n onDelta,\n onSet,\n formatValue,\n className,\n disabled,\n sensitivity,\n min,\n max,\n}: DraggableValueProps) {\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState('');\n const isDragging = useRef(false);\n const hasDragged = useRef(false);\n const startX = useRef(0);\n const startY = useRef(0);\n const lastX = useRef(0);\n const lastY = useRef(0);\n const accumulatedDelta = useRef(0);\n const onDeltaRef = useRef(onDelta);\n onDeltaRef.current = onDelta;\n\n const handleMouseDown = useCallback(\n (e: React.MouseEvent) => {\n if (disabled || isEditing) return;\n\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = e.clientX;\n lastX.current = e.clientX;\n accumulatedDelta.current = 0;\n document.body.style.cursor = 'ew-resize';\n document.body.style.userSelect = 'none';\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!isDragging.current) return;\n\n const totalDeltaX = Math.abs(e.clientX - startX.current);\n if (totalDeltaX > DRAG_THRESHOLD) {\n hasDragged.current = true;\n }\n\n const deltaX = e.clientX - lastX.current;\n const deltaValue = deltaX / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = e.clientX;\n };\n\n const handleMouseUp = () => {\n isDragging.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const handleTouchStart = useCallback(\n (e: React.TouchEvent) => {\n if (disabled || isEditing) return;\n\n const touch = e.touches[0];\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = touch.clientX;\n startY.current = touch.clientY;\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n\n const handleTouchMove = (e: TouchEvent) => {\n if (!isDragging.current) return;\n\n const touch = e.touches[0];\n const totalDeltaX = Math.abs(touch.clientX - startX.current);\n const totalDeltaY = Math.abs(touch.clientY - startY.current);\n if (totalDeltaX > DRAG_THRESHOLD || totalDeltaY > DRAG_THRESHOLD) {\n hasDragged.current = true;\n e.preventDefault(); // Prevent scrolling once dragging starts\n }\n\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n // Horizontal: right = increase, left = decrease\n // Vertical: up = increase, down = decrease\n const deltaValue = (deltaX - deltaY) / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n };\n\n const handleTouchEnd = () => {\n isDragging.current = false;\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n };\n\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const displayValue = formatValue ? formatValue(value) : String(value);\n\n const handleClick = useCallback(() => {\n if (disabled || hasDragged.current) return;\n setEditValue(displayValue);\n setIsEditing(true);\n }, [disabled, displayValue]);\n\n const handleDivKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n if (/^[0-9]$/.test(e.key)) {\n e.preventDefault();\n setEditValue(e.key);\n setIsEditing(true);\n }\n },\n [disabled]\n );\n\n const commitEdit = useCallback(() => {\n const parsed = parseInt(editValue, 10);\n if (!isNaN(parsed)) {\n const clamped = Math.max(min, Math.min(max, parsed));\n onSet(clamped);\n }\n setIsEditing(false);\n }, [editValue, min, max, onSet]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') {\n commitEdit();\n } else if (e.key === 'Escape') {\n setIsEditing(false);\n }\n },\n [commitEdit]\n );\n\n if (isEditing) {\n return (\n <input\n type=\"text\"\n inputMode=\"numeric\"\n value={editValue}\n onChange={(e) => setEditValue(e.target.value.replace(/\\D/g, '').slice(-2))}\n onFocus={(e) => e.target.select()}\n onBlur={commitEdit}\n onKeyDown={handleKeyDown}\n className={`scrubtime-value scrubtime-value--editing ${className || ''}`}\n autoFocus\n />\n );\n }\n\n return (\n <div\n onMouseDown={handleMouseDown}\n onTouchStart={handleTouchStart}\n onClick={handleClick}\n onKeyDown={handleDivKeyDown}\n className={`scrubtime-value ${className || ''} ${disabled ? 'scrubtime-value--disabled' : ''}`}\n role=\"spinbutton\"\n aria-valuenow={value}\n aria-disabled={disabled}\n tabIndex={disabled ? -1 : 0}\n >\n {displayValue}\n </div>\n );\n}\n\nfunction parseTime(time: string): { hours: number; minutes: number } {\n const [h, m] = time.split(':').map(Number);\n return { hours: h || 0, minutes: m || 0 };\n}\n\nfunction formatTime(hours: number, minutes: number): string {\n return `${hours}:${String(minutes).padStart(2, '0')}`;\n}\n\nfunction clampTotalMinutes(totalMins: number): number {\n return Math.max(0, Math.min(23 * 60 + 59, totalMins));\n}\n\nexport function TimePicker({\n value,\n onChange,\n label,\n className,\n disabled = false,\n sliderStep = 15,\n dragSensitivity = 3,\n divisions = 4,\n}: TimePickerProps) {\n const labelCount = divisions + 1;\n const { hours, minutes } = parseTime(value);\n const totalMinutes = hours * 60 + minutes;\n\n const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const mins = clampTotalMinutes(parseInt(e.target.value));\n const h = Math.floor(mins / 60);\n const m = mins % 60;\n onChange(formatTime(h, m));\n };\n\n const handleHoursDelta = useCallback(\n (delta: number) => {\n const newHours = Math.max(0, Math.min(23, hours + delta));\n onChange(formatTime(newHours, minutes));\n },\n [hours, minutes, onChange]\n );\n\n const handleHoursSet = useCallback(\n (newHours: number) => {\n onChange(formatTime(newHours, minutes));\n },\n [minutes, onChange]\n );\n\n const handleMinutesDelta = useCallback(\n (delta: number) => {\n const newTotalMinutes = clampTotalMinutes(totalMinutes + delta);\n const h = Math.floor(newTotalMinutes / 60);\n const m = newTotalMinutes % 60;\n onChange(formatTime(h, m));\n },\n [totalMinutes, onChange]\n );\n\n const handleMinutesSet = useCallback(\n (newMinutes: number) => {\n onChange(formatTime(hours, newMinutes));\n },\n [hours, onChange]\n );\n\n return (\n <div className={`scrubtime ${className || ''} ${disabled ? 'scrubtime--disabled' : ''}`}>\n {label && <label className=\"scrubtime-label\">{label}</label>}\n\n <div className=\"scrubtime-container\">\n <div className=\"scrubtime-display\">\n <DraggableValue\n value={hours}\n onDelta={handleHoursDelta}\n onSet={handleHoursSet}\n disabled={disabled}\n sensitivity={dragSensitivity * 2}\n min={0}\n max={23}\n className=\"scrubtime-hours\"\n />\n <span className=\"scrubtime-separator\">:</span>\n <DraggableValue\n value={minutes}\n onDelta={handleMinutesDelta}\n onSet={handleMinutesSet}\n formatValue={(v) => String(v).padStart(2, '0')}\n disabled={disabled}\n sensitivity={dragSensitivity / 2}\n min={0}\n max={59}\n className=\"scrubtime-minutes\"\n />\n </div>\n\n <div className=\"scrubtime-slider-container\">\n <input\n type=\"range\"\n min={0}\n max={23 * 60 + 59}\n step={sliderStep}\n value={totalMinutes}\n onChange={handleSliderChange}\n disabled={disabled}\n tabIndex={-1}\n className=\"scrubtime-slider\"\n aria-label=\"Time slider\"\n />\n <div className=\"scrubtime-slider-labels\">\n {Array.from({ length: labelCount }, (_, i) => {\n const hour = Math.round((24 / divisions) * i);\n const percent = (i / divisions) * 100;\n return (\n <span key={i} style={{ left: `${percent}%` }}>\n {hour}\n </span>\n );\n })}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["e","touch"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/TimePicker.tsx"],"sourcesContent":["export { TimePicker } from './TimePicker';\nexport type { TimePickerProps } from './TimePicker';\n","import { useRef, useCallback, useState } from 'react';\n\nexport interface TimePickerProps {\n /** Current time value in \"H:mm\" or \"HH:mm\" format */\n value: string;\n /** Callback fired when time changes */\n onChange: (value: string) => void;\n /** Optional label displayed above the picker */\n label?: string;\n /** Custom class name for the root element */\n className?: string;\n /** Disable the picker */\n disabled?: boolean;\n /** Slider step in minutes (default: 15) */\n sliderStep?: number;\n /** Drag sensitivity - pixels per unit change (default: 3) */\n dragSensitivity?: number;\n /** Number of equal parts to divide the 24h range into (default: 4 = labels at 0,6,12,18,24) */\n divisions?: number;\n}\n\ninterface DraggableValueProps {\n value: number;\n onDelta: (delta: number) => void;\n onSet: (value: number) => void;\n formatValue?: (v: number) => string;\n className?: string;\n disabled?: boolean;\n sensitivity: number;\n min: number;\n max: number;\n}\n\nconst DRAG_THRESHOLD = 3;\n\nfunction DraggableValue({\n value,\n onDelta,\n onSet,\n formatValue,\n className,\n disabled,\n sensitivity,\n min,\n max,\n}: DraggableValueProps) {\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState('');\n const [showMagnifier, setShowMagnifier] = useState(false);\n const isDragging = useRef(false);\n const hasDragged = useRef(false);\n const isLongPress = useRef(false);\n const needsBuffer = useRef(false);\n const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const startX = useRef(0);\n const startY = useRef(0);\n const lastX = useRef(0);\n const lastY = useRef(0);\n const accumulatedDelta = useRef(0);\n const onDeltaRef = useRef(onDelta);\n onDeltaRef.current = onDelta;\n\n const handleMouseDown = useCallback(\n (e: React.MouseEvent) => {\n if (disabled || isEditing) return;\n\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = e.clientX;\n lastX.current = e.clientX;\n accumulatedDelta.current = 0;\n document.body.style.cursor = 'ew-resize';\n document.body.style.userSelect = 'none';\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!isDragging.current) return;\n\n const totalDeltaX = Math.abs(e.clientX - startX.current);\n if (totalDeltaX > DRAG_THRESHOLD) {\n hasDragged.current = true;\n }\n\n const deltaX = e.clientX - lastX.current;\n const deltaValue = deltaX / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = e.clientX;\n };\n\n const handleMouseUp = () => {\n isDragging.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const handleTouchStart = useCallback(\n (e: React.TouchEvent) => {\n if (disabled || isEditing) return;\n\n const touch = e.touches[0];\n isDragging.current = true;\n hasDragged.current = false;\n isLongPress.current = false;\n startX.current = touch.clientX;\n startY.current = touch.clientY;\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n\n // Long press timer - activates after 300ms, no threshold needed after\n longPressTimer.current = setTimeout(() => {\n isLongPress.current = true;\n setShowMagnifier(true);\n }, 300);\n\n const handleTouchMove = (e: TouchEvent) => {\n if (!isDragging.current) return;\n\n const touch = e.touches[0];\n\n // Before drag is activated, check threshold or long press\n if (!hasDragged.current) {\n const totalDeltaX = touch.clientX - startX.current;\n const totalDeltaY = touch.clientY - startY.current;\n\n // Long press activated - start immediately with +1/-1 on any movement\n if (isLongPress.current) {\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n if (Math.abs(deltaX) > 0 || Math.abs(deltaY) > 0) {\n hasDragged.current = true;\n e.preventDefault();\n const direction = (deltaX + deltaY) > 0 ? 1 : -1;\n onDeltaRef.current(direction);\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n // Add buffer to prevent rapid-fire changes after activation\n accumulatedDelta.current = -direction * 0.8;\n return;\n }\n }\n\n if (Math.abs(totalDeltaX) > DRAG_THRESHOLD || Math.abs(totalDeltaY) > DRAG_THRESHOLD) {\n // Cancel long press timer since drag started\n if (longPressTimer.current) {\n clearTimeout(longPressTimer.current);\n longPressTimer.current = null;\n }\n hasDragged.current = true;\n needsBuffer.current = true;\n setShowMagnifier(true);\n e.preventDefault();\n // Show magnifier first, don't change value yet - further dragging will mutate\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n }\n // Always update lastX/lastY so long press activation has fresh position\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n return;\n }\n\n e.preventDefault();\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n // Horizontal: right = increase, left = decrease\n // Vertical: up = increase, down = decrease\n const deltaValue = (deltaX + deltaY) / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n // Require more movement for first change after distance activation\n const threshold = needsBuffer.current ? 2 : 1;\n if (Math.abs(accumulatedDelta.current) >= threshold) {\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n needsBuffer.current = false;\n }\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n };\n\n const handleTouchEnd = () => {\n isDragging.current = false;\n needsBuffer.current = false;\n if (longPressTimer.current) {\n clearTimeout(longPressTimer.current);\n longPressTimer.current = null;\n }\n setShowMagnifier(false);\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n };\n\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const displayValue = formatValue ? formatValue(value) : String(value);\n\n const handleClick = useCallback(() => {\n if (disabled || hasDragged.current) return;\n setEditValue(displayValue);\n setIsEditing(true);\n }, [disabled, displayValue]);\n\n const handleDivKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n if (/^[0-9]$/.test(e.key)) {\n e.preventDefault();\n setEditValue(e.key);\n setIsEditing(true);\n }\n },\n [disabled]\n );\n\n const commitEdit = useCallback(() => {\n const parsed = parseInt(editValue, 10);\n if (!isNaN(parsed)) {\n const clamped = Math.max(min, Math.min(max, parsed));\n onSet(clamped);\n }\n setIsEditing(false);\n }, [editValue, min, max, onSet]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') {\n commitEdit();\n } else if (e.key === 'Escape') {\n setIsEditing(false);\n }\n },\n [commitEdit]\n );\n\n if (isEditing) {\n return (\n <input\n type=\"text\"\n inputMode=\"numeric\"\n value={editValue}\n onChange={(e) => setEditValue(e.target.value.replace(/\\D/g, '').slice(-2))}\n onFocus={(e) => e.target.select()}\n onBlur={commitEdit}\n onKeyDown={handleKeyDown}\n className={`scrubtime-value scrubtime-value--editing ${className || ''}`}\n autoFocus\n />\n );\n }\n\n return (\n <div className=\"scrubtime-value-wrapper\">\n {showMagnifier && (\n <div className=\"scrubtime-magnifier\" aria-hidden=\"true\">\n {displayValue}\n </div>\n )}\n <div\n onMouseDown={handleMouseDown}\n onTouchStart={handleTouchStart}\n onClick={handleClick}\n onKeyDown={handleDivKeyDown}\n className={`scrubtime-value ${className || ''} ${disabled ? 'scrubtime-value--disabled' : ''}`}\n role=\"spinbutton\"\n aria-valuenow={value}\n aria-disabled={disabled}\n tabIndex={disabled ? -1 : 0}\n >\n {displayValue}\n </div>\n </div>\n );\n}\n\nfunction parseTime(time: string): { hours: number; minutes: number } {\n const [h, m] = time.split(':').map(Number);\n return { hours: h || 0, minutes: m || 0 };\n}\n\nfunction formatTime(hours: number, minutes: number): string {\n return `${hours}:${String(minutes).padStart(2, '0')}`;\n}\n\nfunction clampTotalMinutes(totalMins: number): number {\n return Math.max(0, Math.min(23 * 60 + 59, totalMins));\n}\n\nexport function TimePicker({\n value,\n onChange,\n label,\n className,\n disabled = false,\n sliderStep = 15,\n dragSensitivity = 3,\n divisions = 4,\n}: TimePickerProps) {\n const labelCount = divisions + 1;\n const { hours, minutes } = parseTime(value);\n const totalMinutes = hours * 60 + minutes;\n\n const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const mins = clampTotalMinutes(parseInt(e.target.value));\n const h = Math.floor(mins / 60);\n const m = mins % 60;\n onChange(formatTime(h, m));\n };\n\n const handleHoursDelta = useCallback(\n (delta: number) => {\n const newHours = Math.max(0, Math.min(23, hours + delta));\n onChange(formatTime(newHours, minutes));\n },\n [hours, minutes, onChange]\n );\n\n const handleHoursSet = useCallback(\n (newHours: number) => {\n onChange(formatTime(newHours, minutes));\n },\n [minutes, onChange]\n );\n\n const handleMinutesDelta = useCallback(\n (delta: number) => {\n const newTotalMinutes = clampTotalMinutes(totalMinutes + delta);\n const h = Math.floor(newTotalMinutes / 60);\n const m = newTotalMinutes % 60;\n onChange(formatTime(h, m));\n },\n [totalMinutes, onChange]\n );\n\n const handleMinutesSet = useCallback(\n (newMinutes: number) => {\n onChange(formatTime(hours, newMinutes));\n },\n [hours, onChange]\n );\n\n return (\n <div className={`scrubtime ${className || ''} ${disabled ? 'scrubtime--disabled' : ''}`}>\n {label && <label className=\"scrubtime-label\">{label}</label>}\n\n <div className=\"scrubtime-container\">\n <div className=\"scrubtime-display\">\n <DraggableValue\n value={hours}\n onDelta={handleHoursDelta}\n onSet={handleHoursSet}\n disabled={disabled}\n sensitivity={dragSensitivity * 2}\n min={0}\n max={23}\n className=\"scrubtime-hours\"\n />\n <span className=\"scrubtime-separator\">:</span>\n <DraggableValue\n value={minutes}\n onDelta={handleMinutesDelta}\n onSet={handleMinutesSet}\n formatValue={(v) => String(v).padStart(2, '0')}\n disabled={disabled}\n sensitivity={dragSensitivity}\n min={0}\n max={59}\n className=\"scrubtime-minutes\"\n />\n </div>\n\n <div className=\"scrubtime-slider-container\">\n <input\n type=\"range\"\n min={0}\n max={23 * 60 + 59}\n step={sliderStep}\n value={totalMinutes}\n onChange={handleSliderChange}\n disabled={disabled}\n tabIndex={-1}\n className=\"scrubtime-slider\"\n aria-label=\"Time slider\"\n />\n <div className=\"scrubtime-slider-labels\">\n {Array.from({ length: labelCount }, (_, i) => {\n const hour = Math.round((24 / divisions) * i);\n const percent = (i / divisions) * 100;\n return (\n <span key={i} style={{ left: `${percent}%` }}>\n {hour}\n </span>\n );\n })}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["e","touch","deltaX","deltaY"]}
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,12 @@ function DraggableValue({
|
|
|
15
15
|
}) {
|
|
16
16
|
const [isEditing, setIsEditing] = useState(false);
|
|
17
17
|
const [editValue, setEditValue] = useState("");
|
|
18
|
+
const [showMagnifier, setShowMagnifier] = useState(false);
|
|
18
19
|
const isDragging = useRef(false);
|
|
19
20
|
const hasDragged = useRef(false);
|
|
21
|
+
const isLongPress = useRef(false);
|
|
22
|
+
const needsBuffer = useRef(false);
|
|
23
|
+
const longPressTimer = useRef(null);
|
|
20
24
|
const startX = useRef(0);
|
|
21
25
|
const startY = useRef(0);
|
|
22
26
|
const lastX = useRef(0);
|
|
@@ -68,34 +72,76 @@ function DraggableValue({
|
|
|
68
72
|
const touch = e.touches[0];
|
|
69
73
|
isDragging.current = true;
|
|
70
74
|
hasDragged.current = false;
|
|
75
|
+
isLongPress.current = false;
|
|
71
76
|
startX.current = touch.clientX;
|
|
72
77
|
startY.current = touch.clientY;
|
|
73
78
|
lastX.current = touch.clientX;
|
|
74
79
|
lastY.current = touch.clientY;
|
|
75
80
|
accumulatedDelta.current = 0;
|
|
81
|
+
longPressTimer.current = setTimeout(() => {
|
|
82
|
+
isLongPress.current = true;
|
|
83
|
+
setShowMagnifier(true);
|
|
84
|
+
}, 300);
|
|
76
85
|
const handleTouchMove = (e2) => {
|
|
77
86
|
if (!isDragging.current) return;
|
|
78
87
|
const touch2 = e2.touches[0];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
if (!hasDragged.current) {
|
|
89
|
+
const totalDeltaX = touch2.clientX - startX.current;
|
|
90
|
+
const totalDeltaY = touch2.clientY - startY.current;
|
|
91
|
+
if (isLongPress.current) {
|
|
92
|
+
const deltaX2 = touch2.clientX - lastX.current;
|
|
93
|
+
const deltaY2 = touch2.clientY - lastY.current;
|
|
94
|
+
if (Math.abs(deltaX2) > 0 || Math.abs(deltaY2) > 0) {
|
|
95
|
+
hasDragged.current = true;
|
|
96
|
+
e2.preventDefault();
|
|
97
|
+
const direction = deltaX2 + deltaY2 > 0 ? 1 : -1;
|
|
98
|
+
onDeltaRef.current(direction);
|
|
99
|
+
lastX.current = touch2.clientX;
|
|
100
|
+
lastY.current = touch2.clientY;
|
|
101
|
+
accumulatedDelta.current = -direction * 0.8;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (Math.abs(totalDeltaX) > DRAG_THRESHOLD || Math.abs(totalDeltaY) > DRAG_THRESHOLD) {
|
|
106
|
+
if (longPressTimer.current) {
|
|
107
|
+
clearTimeout(longPressTimer.current);
|
|
108
|
+
longPressTimer.current = null;
|
|
109
|
+
}
|
|
110
|
+
hasDragged.current = true;
|
|
111
|
+
needsBuffer.current = true;
|
|
112
|
+
setShowMagnifier(true);
|
|
113
|
+
e2.preventDefault();
|
|
114
|
+
lastX.current = touch2.clientX;
|
|
115
|
+
lastY.current = touch2.clientY;
|
|
116
|
+
accumulatedDelta.current = 0;
|
|
117
|
+
}
|
|
118
|
+
lastX.current = touch2.clientX;
|
|
119
|
+
lastY.current = touch2.clientY;
|
|
120
|
+
return;
|
|
84
121
|
}
|
|
122
|
+
e2.preventDefault();
|
|
85
123
|
const deltaX = touch2.clientX - lastX.current;
|
|
86
124
|
const deltaY = touch2.clientY - lastY.current;
|
|
87
|
-
const deltaValue = (deltaX
|
|
125
|
+
const deltaValue = (deltaX + deltaY) / sensitivity;
|
|
88
126
|
accumulatedDelta.current += deltaValue;
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
127
|
+
const threshold = needsBuffer.current ? 2 : 1;
|
|
128
|
+
if (Math.abs(accumulatedDelta.current) >= threshold) {
|
|
129
|
+
const wholeDelta = Math.trunc(accumulatedDelta.current);
|
|
91
130
|
accumulatedDelta.current -= wholeDelta;
|
|
92
131
|
onDeltaRef.current(wholeDelta);
|
|
132
|
+
needsBuffer.current = false;
|
|
93
133
|
}
|
|
94
134
|
lastX.current = touch2.clientX;
|
|
95
135
|
lastY.current = touch2.clientY;
|
|
96
136
|
};
|
|
97
137
|
const handleTouchEnd = () => {
|
|
98
138
|
isDragging.current = false;
|
|
139
|
+
needsBuffer.current = false;
|
|
140
|
+
if (longPressTimer.current) {
|
|
141
|
+
clearTimeout(longPressTimer.current);
|
|
142
|
+
longPressTimer.current = null;
|
|
143
|
+
}
|
|
144
|
+
setShowMagnifier(false);
|
|
99
145
|
document.removeEventListener("touchmove", handleTouchMove);
|
|
100
146
|
document.removeEventListener("touchend", handleTouchEnd);
|
|
101
147
|
};
|
|
@@ -155,21 +201,24 @@ function DraggableValue({
|
|
|
155
201
|
}
|
|
156
202
|
);
|
|
157
203
|
}
|
|
158
|
-
return /* @__PURE__ */
|
|
159
|
-
"div",
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
204
|
+
return /* @__PURE__ */ jsxs("div", { className: "scrubtime-value-wrapper", children: [
|
|
205
|
+
showMagnifier && /* @__PURE__ */ jsx("div", { className: "scrubtime-magnifier", "aria-hidden": "true", children: displayValue }),
|
|
206
|
+
/* @__PURE__ */ jsx(
|
|
207
|
+
"div",
|
|
208
|
+
{
|
|
209
|
+
onMouseDown: handleMouseDown,
|
|
210
|
+
onTouchStart: handleTouchStart,
|
|
211
|
+
onClick: handleClick,
|
|
212
|
+
onKeyDown: handleDivKeyDown,
|
|
213
|
+
className: `scrubtime-value ${className || ""} ${disabled ? "scrubtime-value--disabled" : ""}`,
|
|
214
|
+
role: "spinbutton",
|
|
215
|
+
"aria-valuenow": value,
|
|
216
|
+
"aria-disabled": disabled,
|
|
217
|
+
tabIndex: disabled ? -1 : 0,
|
|
218
|
+
children: displayValue
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
] });
|
|
173
222
|
}
|
|
174
223
|
function parseTime(time) {
|
|
175
224
|
const [h, m] = time.split(":").map(Number);
|
|
@@ -255,7 +304,7 @@ function TimePicker({
|
|
|
255
304
|
onSet: handleMinutesSet,
|
|
256
305
|
formatValue: (v) => String(v).padStart(2, "0"),
|
|
257
306
|
disabled,
|
|
258
|
-
sensitivity: dragSensitivity
|
|
307
|
+
sensitivity: dragSensitivity,
|
|
259
308
|
min: 0,
|
|
260
309
|
max: 59,
|
|
261
310
|
className: "scrubtime-minutes"
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/TimePicker.tsx"],"sourcesContent":["import { useRef, useCallback, useState } from 'react';\n\nexport interface TimePickerProps {\n /** Current time value in \"H:mm\" or \"HH:mm\" format */\n value: string;\n /** Callback fired when time changes */\n onChange: (value: string) => void;\n /** Optional label displayed above the picker */\n label?: string;\n /** Custom class name for the root element */\n className?: string;\n /** Disable the picker */\n disabled?: boolean;\n /** Slider step in minutes (default: 15) */\n sliderStep?: number;\n /** Drag sensitivity - pixels per unit change (default: 3) */\n dragSensitivity?: number;\n /** Number of equal parts to divide the 24h range into (default: 4 = labels at 0,6,12,18,24) */\n divisions?: number;\n}\n\ninterface DraggableValueProps {\n value: number;\n onDelta: (delta: number) => void;\n onSet: (value: number) => void;\n formatValue?: (v: number) => string;\n className?: string;\n disabled?: boolean;\n sensitivity: number;\n min: number;\n max: number;\n}\n\nconst DRAG_THRESHOLD = 3;\n\nfunction DraggableValue({\n value,\n onDelta,\n onSet,\n formatValue,\n className,\n disabled,\n sensitivity,\n min,\n max,\n}: DraggableValueProps) {\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState('');\n const isDragging = useRef(false);\n const hasDragged = useRef(false);\n const startX = useRef(0);\n const startY = useRef(0);\n const lastX = useRef(0);\n const lastY = useRef(0);\n const accumulatedDelta = useRef(0);\n const onDeltaRef = useRef(onDelta);\n onDeltaRef.current = onDelta;\n\n const handleMouseDown = useCallback(\n (e: React.MouseEvent) => {\n if (disabled || isEditing) return;\n\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = e.clientX;\n lastX.current = e.clientX;\n accumulatedDelta.current = 0;\n document.body.style.cursor = 'ew-resize';\n document.body.style.userSelect = 'none';\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!isDragging.current) return;\n\n const totalDeltaX = Math.abs(e.clientX - startX.current);\n if (totalDeltaX > DRAG_THRESHOLD) {\n hasDragged.current = true;\n }\n\n const deltaX = e.clientX - lastX.current;\n const deltaValue = deltaX / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = e.clientX;\n };\n\n const handleMouseUp = () => {\n isDragging.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const handleTouchStart = useCallback(\n (e: React.TouchEvent) => {\n if (disabled || isEditing) return;\n\n const touch = e.touches[0];\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = touch.clientX;\n startY.current = touch.clientY;\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n\n const handleTouchMove = (e: TouchEvent) => {\n if (!isDragging.current) return;\n\n const touch = e.touches[0];\n const totalDeltaX = Math.abs(touch.clientX - startX.current);\n const totalDeltaY = Math.abs(touch.clientY - startY.current);\n if (totalDeltaX > DRAG_THRESHOLD || totalDeltaY > DRAG_THRESHOLD) {\n hasDragged.current = true;\n e.preventDefault(); // Prevent scrolling once dragging starts\n }\n\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n // Horizontal: right = increase, left = decrease\n // Vertical: up = increase, down = decrease\n const deltaValue = (deltaX - deltaY) / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n };\n\n const handleTouchEnd = () => {\n isDragging.current = false;\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n };\n\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const displayValue = formatValue ? formatValue(value) : String(value);\n\n const handleClick = useCallback(() => {\n if (disabled || hasDragged.current) return;\n setEditValue(displayValue);\n setIsEditing(true);\n }, [disabled, displayValue]);\n\n const handleDivKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n if (/^[0-9]$/.test(e.key)) {\n e.preventDefault();\n setEditValue(e.key);\n setIsEditing(true);\n }\n },\n [disabled]\n );\n\n const commitEdit = useCallback(() => {\n const parsed = parseInt(editValue, 10);\n if (!isNaN(parsed)) {\n const clamped = Math.max(min, Math.min(max, parsed));\n onSet(clamped);\n }\n setIsEditing(false);\n }, [editValue, min, max, onSet]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') {\n commitEdit();\n } else if (e.key === 'Escape') {\n setIsEditing(false);\n }\n },\n [commitEdit]\n );\n\n if (isEditing) {\n return (\n <input\n type=\"text\"\n inputMode=\"numeric\"\n value={editValue}\n onChange={(e) => setEditValue(e.target.value.replace(/\\D/g, '').slice(-2))}\n onFocus={(e) => e.target.select()}\n onBlur={commitEdit}\n onKeyDown={handleKeyDown}\n className={`scrubtime-value scrubtime-value--editing ${className || ''}`}\n autoFocus\n />\n );\n }\n\n return (\n <div\n onMouseDown={handleMouseDown}\n onTouchStart={handleTouchStart}\n onClick={handleClick}\n onKeyDown={handleDivKeyDown}\n className={`scrubtime-value ${className || ''} ${disabled ? 'scrubtime-value--disabled' : ''}`}\n role=\"spinbutton\"\n aria-valuenow={value}\n aria-disabled={disabled}\n tabIndex={disabled ? -1 : 0}\n >\n {displayValue}\n </div>\n );\n}\n\nfunction parseTime(time: string): { hours: number; minutes: number } {\n const [h, m] = time.split(':').map(Number);\n return { hours: h || 0, minutes: m || 0 };\n}\n\nfunction formatTime(hours: number, minutes: number): string {\n return `${hours}:${String(minutes).padStart(2, '0')}`;\n}\n\nfunction clampTotalMinutes(totalMins: number): number {\n return Math.max(0, Math.min(23 * 60 + 59, totalMins));\n}\n\nexport function TimePicker({\n value,\n onChange,\n label,\n className,\n disabled = false,\n sliderStep = 15,\n dragSensitivity = 3,\n divisions = 4,\n}: TimePickerProps) {\n const labelCount = divisions + 1;\n const { hours, minutes } = parseTime(value);\n const totalMinutes = hours * 60 + minutes;\n\n const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const mins = clampTotalMinutes(parseInt(e.target.value));\n const h = Math.floor(mins / 60);\n const m = mins % 60;\n onChange(formatTime(h, m));\n };\n\n const handleHoursDelta = useCallback(\n (delta: number) => {\n const newHours = Math.max(0, Math.min(23, hours + delta));\n onChange(formatTime(newHours, minutes));\n },\n [hours, minutes, onChange]\n );\n\n const handleHoursSet = useCallback(\n (newHours: number) => {\n onChange(formatTime(newHours, minutes));\n },\n [minutes, onChange]\n );\n\n const handleMinutesDelta = useCallback(\n (delta: number) => {\n const newTotalMinutes = clampTotalMinutes(totalMinutes + delta);\n const h = Math.floor(newTotalMinutes / 60);\n const m = newTotalMinutes % 60;\n onChange(formatTime(h, m));\n },\n [totalMinutes, onChange]\n );\n\n const handleMinutesSet = useCallback(\n (newMinutes: number) => {\n onChange(formatTime(hours, newMinutes));\n },\n [hours, onChange]\n );\n\n return (\n <div className={`scrubtime ${className || ''} ${disabled ? 'scrubtime--disabled' : ''}`}>\n {label && <label className=\"scrubtime-label\">{label}</label>}\n\n <div className=\"scrubtime-container\">\n <div className=\"scrubtime-display\">\n <DraggableValue\n value={hours}\n onDelta={handleHoursDelta}\n onSet={handleHoursSet}\n disabled={disabled}\n sensitivity={dragSensitivity * 2}\n min={0}\n max={23}\n className=\"scrubtime-hours\"\n />\n <span className=\"scrubtime-separator\">:</span>\n <DraggableValue\n value={minutes}\n onDelta={handleMinutesDelta}\n onSet={handleMinutesSet}\n formatValue={(v) => String(v).padStart(2, '0')}\n disabled={disabled}\n sensitivity={dragSensitivity / 2}\n min={0}\n max={59}\n className=\"scrubtime-minutes\"\n />\n </div>\n\n <div className=\"scrubtime-slider-container\">\n <input\n type=\"range\"\n min={0}\n max={23 * 60 + 59}\n step={sliderStep}\n value={totalMinutes}\n onChange={handleSliderChange}\n disabled={disabled}\n tabIndex={-1}\n className=\"scrubtime-slider\"\n aria-label=\"Time slider\"\n />\n <div className=\"scrubtime-slider-labels\">\n {Array.from({ length: labelCount }, (_, i) => {\n const hour = Math.round((24 / divisions) * i);\n const percent = (i / divisions) * 100;\n return (\n <span key={i} style={{ left: `${percent}%` }}>\n {hour}\n </span>\n );\n })}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";AAAA,SAAS,QAAQ,aAAa,gBAAgB;AAsMxC,cAuGE,YAvGF;AArKN,IAAM,iBAAiB;AAEvB,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,EAAE;AAC7C,QAAM,aAAa,OAAO,KAAK;AAC/B,QAAM,aAAa,OAAO,KAAK;AAC/B,QAAM,SAAS,OAAO,CAAC;AACvB,QAAM,SAAS,OAAO,CAAC;AACvB,QAAM,QAAQ,OAAO,CAAC;AACtB,QAAM,QAAQ,OAAO,CAAC;AACtB,QAAM,mBAAmB,OAAO,CAAC;AACjC,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AAErB,QAAM,kBAAkB;AAAA,IACtB,CAAC,MAAwB;AACvB,UAAI,YAAY,UAAW;AAE3B,iBAAW,UAAU;AACrB,iBAAW,UAAU;AACrB,aAAO,UAAU,EAAE;AACnB,YAAM,UAAU,EAAE;AAClB,uBAAiB,UAAU;AAC3B,eAAS,KAAK,MAAM,SAAS;AAC7B,eAAS,KAAK,MAAM,aAAa;AAEjC,YAAM,kBAAkB,CAACA,OAAkB;AACzC,YAAI,CAAC,WAAW,QAAS;AAEzB,cAAM,cAAc,KAAK,IAAIA,GAAE,UAAU,OAAO,OAAO;AACvD,YAAI,cAAc,gBAAgB;AAChC,qBAAW,UAAU;AAAA,QACvB;AAEA,cAAM,SAASA,GAAE,UAAU,MAAM;AACjC,cAAM,aAAa,SAAS;AAC5B,yBAAiB,WAAW;AAE5B,cAAM,aAAa,KAAK,MAAM,iBAAiB,OAAO;AACtD,YAAI,eAAe,GAAG;AACpB,2BAAiB,WAAW;AAC5B,qBAAW,QAAQ,UAAU;AAAA,QAC/B;AACA,cAAM,UAAUA,GAAE;AAAA,MACpB;AAEA,YAAM,gBAAgB,MAAM;AAC1B,mBAAW,UAAU;AACrB,iBAAS,KAAK,MAAM,SAAS;AAC7B,iBAAS,KAAK,MAAM,aAAa;AACjC,iBAAS,oBAAoB,aAAa,eAAe;AACzD,iBAAS,oBAAoB,WAAW,aAAa;AAAA,MACvD;AAEA,eAAS,iBAAiB,aAAa,eAAe;AACtD,eAAS,iBAAiB,WAAW,aAAa;AAAA,IACpD;AAAA,IACA,CAAC,UAAU,aAAa,SAAS;AAAA,EACnC;AAEA,QAAM,mBAAmB;AAAA,IACvB,CAAC,MAAwB;AACvB,UAAI,YAAY,UAAW;AAE3B,YAAM,QAAQ,EAAE,QAAQ,CAAC;AACzB,iBAAW,UAAU;AACrB,iBAAW,UAAU;AACrB,aAAO,UAAU,MAAM;AACvB,aAAO,UAAU,MAAM;AACvB,YAAM,UAAU,MAAM;AACtB,YAAM,UAAU,MAAM;AACtB,uBAAiB,UAAU;AAE3B,YAAM,kBAAkB,CAACA,OAAkB;AACzC,YAAI,CAAC,WAAW,QAAS;AAEzB,cAAMC,SAAQD,GAAE,QAAQ,CAAC;AACzB,cAAM,cAAc,KAAK,IAAIC,OAAM,UAAU,OAAO,OAAO;AAC3D,cAAM,cAAc,KAAK,IAAIA,OAAM,UAAU,OAAO,OAAO;AAC3D,YAAI,cAAc,kBAAkB,cAAc,gBAAgB;AAChE,qBAAW,UAAU;AACrB,UAAAD,GAAE,eAAe;AAAA,QACnB;AAEA,cAAM,SAASC,OAAM,UAAU,MAAM;AACrC,cAAM,SAASA,OAAM,UAAU,MAAM;AAGrC,cAAM,cAAc,SAAS,UAAU;AACvC,yBAAiB,WAAW;AAE5B,cAAM,aAAa,KAAK,MAAM,iBAAiB,OAAO;AACtD,YAAI,eAAe,GAAG;AACpB,2BAAiB,WAAW;AAC5B,qBAAW,QAAQ,UAAU;AAAA,QAC/B;AACA,cAAM,UAAUA,OAAM;AACtB,cAAM,UAAUA,OAAM;AAAA,MACxB;AAEA,YAAM,iBAAiB,MAAM;AAC3B,mBAAW,UAAU;AACrB,iBAAS,oBAAoB,aAAa,eAAe;AACzD,iBAAS,oBAAoB,YAAY,cAAc;AAAA,MACzD;AAEA,eAAS,iBAAiB,aAAa,iBAAiB,EAAE,SAAS,MAAM,CAAC;AAC1E,eAAS,iBAAiB,YAAY,cAAc;AAAA,IACtD;AAAA,IACA,CAAC,UAAU,aAAa,SAAS;AAAA,EACnC;AAEA,QAAM,eAAe,cAAc,YAAY,KAAK,IAAI,OAAO,KAAK;AAEpE,QAAM,cAAc,YAAY,MAAM;AACpC,QAAI,YAAY,WAAW,QAAS;AACpC,iBAAa,YAAY;AACzB,iBAAa,IAAI;AAAA,EACnB,GAAG,CAAC,UAAU,YAAY,CAAC;AAE3B,QAAM,mBAAmB;AAAA,IACvB,CAAC,MAA2B;AAC1B,UAAI,SAAU;AACd,UAAI,UAAU,KAAK,EAAE,GAAG,GAAG;AACzB,UAAE,eAAe;AACjB,qBAAa,EAAE,GAAG;AAClB,qBAAa,IAAI;AAAA,MACnB;AAAA,IACF;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,aAAa,YAAY,MAAM;AACnC,UAAM,SAAS,SAAS,WAAW,EAAE;AACrC,QAAI,CAAC,MAAM,MAAM,GAAG;AAClB,YAAM,UAAU,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,MAAM,CAAC;AACnD,YAAM,OAAO;AAAA,IACf;AACA,iBAAa,KAAK;AAAA,EACpB,GAAG,CAAC,WAAW,KAAK,KAAK,KAAK,CAAC;AAE/B,QAAM,gBAAgB;AAAA,IACpB,CAAC,MAA2B;AAC1B,UAAI,EAAE,QAAQ,SAAS;AACrB,mBAAW;AAAA,MACb,WAAW,EAAE,QAAQ,UAAU;AAC7B,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,MAAI,WAAW;AACb,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,aAAa,EAAE,OAAO,MAAM,QAAQ,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC;AAAA,QACzE,SAAS,CAAC,MAAM,EAAE,OAAO,OAAO;AAAA,QAChC,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,WAAW,4CAA4C,aAAa,EAAE;AAAA,QACtE,WAAS;AAAA;AAAA,IACX;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAa;AAAA,MACb,cAAc;AAAA,MACd,SAAS;AAAA,MACT,WAAW;AAAA,MACX,WAAW,mBAAmB,aAAa,EAAE,IAAI,WAAW,8BAA8B,EAAE;AAAA,MAC5F,MAAK;AAAA,MACL,iBAAe;AAAA,MACf,iBAAe;AAAA,MACf,UAAU,WAAW,KAAK;AAAA,MAEzB;AAAA;AAAA,EACH;AAEJ;AAEA,SAAS,UAAU,MAAkD;AACnE,QAAM,CAAC,GAAG,CAAC,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI,MAAM;AACzC,SAAO,EAAE,OAAO,KAAK,GAAG,SAAS,KAAK,EAAE;AAC1C;AAEA,SAAS,WAAW,OAAe,SAAyB;AAC1D,SAAO,GAAG,KAAK,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,CAAC;AACrD;AAEA,SAAS,kBAAkB,WAA2B;AACpD,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,IAAI,SAAS,CAAC;AACtD;AAEO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,YAAY;AACd,GAAoB;AAClB,QAAM,aAAa,YAAY;AAC/B,QAAM,EAAE,OAAO,QAAQ,IAAI,UAAU,KAAK;AAC1C,QAAM,eAAe,QAAQ,KAAK;AAElC,QAAM,qBAAqB,CAAC,MAA2C;AACrE,QAAI,SAAU;AACd,UAAM,OAAO,kBAAkB,SAAS,EAAE,OAAO,KAAK,CAAC;AACvD,UAAM,IAAI,KAAK,MAAM,OAAO,EAAE;AAC9B,UAAM,IAAI,OAAO;AACjB,aAAS,WAAW,GAAG,CAAC,CAAC;AAAA,EAC3B;AAEA,QAAM,mBAAmB;AAAA,IACvB,CAAC,UAAkB;AACjB,YAAM,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,QAAQ,KAAK,CAAC;AACxD,eAAS,WAAW,UAAU,OAAO,CAAC;AAAA,IACxC;AAAA,IACA,CAAC,OAAO,SAAS,QAAQ;AAAA,EAC3B;AAEA,QAAM,iBAAiB;AAAA,IACrB,CAAC,aAAqB;AACpB,eAAS,WAAW,UAAU,OAAO,CAAC;AAAA,IACxC;AAAA,IACA,CAAC,SAAS,QAAQ;AAAA,EACpB;AAEA,QAAM,qBAAqB;AAAA,IACzB,CAAC,UAAkB;AACjB,YAAM,kBAAkB,kBAAkB,eAAe,KAAK;AAC9D,YAAM,IAAI,KAAK,MAAM,kBAAkB,EAAE;AACzC,YAAM,IAAI,kBAAkB;AAC5B,eAAS,WAAW,GAAG,CAAC,CAAC;AAAA,IAC3B;AAAA,IACA,CAAC,cAAc,QAAQ;AAAA,EACzB;AAEA,QAAM,mBAAmB;AAAA,IACvB,CAAC,eAAuB;AACtB,eAAS,WAAW,OAAO,UAAU,CAAC;AAAA,IACxC;AAAA,IACA,CAAC,OAAO,QAAQ;AAAA,EAClB;AAEA,SACE,qBAAC,SAAI,WAAW,aAAa,aAAa,EAAE,IAAI,WAAW,wBAAwB,EAAE,IAClF;AAAA,aAAS,oBAAC,WAAM,WAAU,mBAAmB,iBAAM;AAAA,IAEpD,qBAAC,SAAI,WAAU,uBACb;AAAA,2BAAC,SAAI,WAAU,qBACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,SAAS;AAAA,YACT,OAAO;AAAA,YACP;AAAA,YACA,aAAa,kBAAkB;AAAA,YAC/B,KAAK;AAAA,YACL,KAAK;AAAA,YACL,WAAU;AAAA;AAAA,QACZ;AAAA,QACA,oBAAC,UAAK,WAAU,uBAAsB,eAAC;AAAA,QACvC;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,SAAS;AAAA,YACT,OAAO;AAAA,YACP,aAAa,CAAC,MAAM,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AAAA,YAC7C;AAAA,YACA,aAAa,kBAAkB;AAAA,YAC/B,KAAK;AAAA,YACL,KAAK;AAAA,YACL,WAAU;AAAA;AAAA,QACZ;AAAA,SACF;AAAA,MAEA,qBAAC,SAAI,WAAU,8BACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,KAAK;AAAA,YACL,KAAK,KAAK,KAAK;AAAA,YACf,MAAM;AAAA,YACN,OAAO;AAAA,YACP,UAAU;AAAA,YACV;AAAA,YACA,UAAU;AAAA,YACV,WAAU;AAAA,YACV,cAAW;AAAA;AAAA,QACb;AAAA,QACA,oBAAC,SAAI,WAAU,2BACZ,gBAAM,KAAK,EAAE,QAAQ,WAAW,GAAG,CAAC,GAAG,MAAM;AAC5C,gBAAM,OAAO,KAAK,MAAO,KAAK,YAAa,CAAC;AAC5C,gBAAM,UAAW,IAAI,YAAa;AAClC,iBACE,oBAAC,UAAa,OAAO,EAAE,MAAM,GAAG,OAAO,IAAI,GACxC,kBADQ,CAEX;AAAA,QAEJ,CAAC,GACH;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":["e","touch"]}
|
|
1
|
+
{"version":3,"sources":["../src/TimePicker.tsx"],"sourcesContent":["import { useRef, useCallback, useState } from 'react';\n\nexport interface TimePickerProps {\n /** Current time value in \"H:mm\" or \"HH:mm\" format */\n value: string;\n /** Callback fired when time changes */\n onChange: (value: string) => void;\n /** Optional label displayed above the picker */\n label?: string;\n /** Custom class name for the root element */\n className?: string;\n /** Disable the picker */\n disabled?: boolean;\n /** Slider step in minutes (default: 15) */\n sliderStep?: number;\n /** Drag sensitivity - pixels per unit change (default: 3) */\n dragSensitivity?: number;\n /** Number of equal parts to divide the 24h range into (default: 4 = labels at 0,6,12,18,24) */\n divisions?: number;\n}\n\ninterface DraggableValueProps {\n value: number;\n onDelta: (delta: number) => void;\n onSet: (value: number) => void;\n formatValue?: (v: number) => string;\n className?: string;\n disabled?: boolean;\n sensitivity: number;\n min: number;\n max: number;\n}\n\nconst DRAG_THRESHOLD = 3;\n\nfunction DraggableValue({\n value,\n onDelta,\n onSet,\n formatValue,\n className,\n disabled,\n sensitivity,\n min,\n max,\n}: DraggableValueProps) {\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState('');\n const [showMagnifier, setShowMagnifier] = useState(false);\n const isDragging = useRef(false);\n const hasDragged = useRef(false);\n const isLongPress = useRef(false);\n const needsBuffer = useRef(false);\n const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const startX = useRef(0);\n const startY = useRef(0);\n const lastX = useRef(0);\n const lastY = useRef(0);\n const accumulatedDelta = useRef(0);\n const onDeltaRef = useRef(onDelta);\n onDeltaRef.current = onDelta;\n\n const handleMouseDown = useCallback(\n (e: React.MouseEvent) => {\n if (disabled || isEditing) return;\n\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = e.clientX;\n lastX.current = e.clientX;\n accumulatedDelta.current = 0;\n document.body.style.cursor = 'ew-resize';\n document.body.style.userSelect = 'none';\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!isDragging.current) return;\n\n const totalDeltaX = Math.abs(e.clientX - startX.current);\n if (totalDeltaX > DRAG_THRESHOLD) {\n hasDragged.current = true;\n }\n\n const deltaX = e.clientX - lastX.current;\n const deltaValue = deltaX / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = e.clientX;\n };\n\n const handleMouseUp = () => {\n isDragging.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const handleTouchStart = useCallback(\n (e: React.TouchEvent) => {\n if (disabled || isEditing) return;\n\n const touch = e.touches[0];\n isDragging.current = true;\n hasDragged.current = false;\n isLongPress.current = false;\n startX.current = touch.clientX;\n startY.current = touch.clientY;\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n\n // Long press timer - activates after 300ms, no threshold needed after\n longPressTimer.current = setTimeout(() => {\n isLongPress.current = true;\n setShowMagnifier(true);\n }, 300);\n\n const handleTouchMove = (e: TouchEvent) => {\n if (!isDragging.current) return;\n\n const touch = e.touches[0];\n\n // Before drag is activated, check threshold or long press\n if (!hasDragged.current) {\n const totalDeltaX = touch.clientX - startX.current;\n const totalDeltaY = touch.clientY - startY.current;\n\n // Long press activated - start immediately with +1/-1 on any movement\n if (isLongPress.current) {\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n if (Math.abs(deltaX) > 0 || Math.abs(deltaY) > 0) {\n hasDragged.current = true;\n e.preventDefault();\n const direction = (deltaX + deltaY) > 0 ? 1 : -1;\n onDeltaRef.current(direction);\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n // Add buffer to prevent rapid-fire changes after activation\n accumulatedDelta.current = -direction * 0.8;\n return;\n }\n }\n\n if (Math.abs(totalDeltaX) > DRAG_THRESHOLD || Math.abs(totalDeltaY) > DRAG_THRESHOLD) {\n // Cancel long press timer since drag started\n if (longPressTimer.current) {\n clearTimeout(longPressTimer.current);\n longPressTimer.current = null;\n }\n hasDragged.current = true;\n needsBuffer.current = true;\n setShowMagnifier(true);\n e.preventDefault();\n // Show magnifier first, don't change value yet - further dragging will mutate\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n }\n // Always update lastX/lastY so long press activation has fresh position\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n return;\n }\n\n e.preventDefault();\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n // Horizontal: right = increase, left = decrease\n // Vertical: up = increase, down = decrease\n const deltaValue = (deltaX + deltaY) / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n // Require more movement for first change after distance activation\n const threshold = needsBuffer.current ? 2 : 1;\n if (Math.abs(accumulatedDelta.current) >= threshold) {\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n needsBuffer.current = false;\n }\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n };\n\n const handleTouchEnd = () => {\n isDragging.current = false;\n needsBuffer.current = false;\n if (longPressTimer.current) {\n clearTimeout(longPressTimer.current);\n longPressTimer.current = null;\n }\n setShowMagnifier(false);\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n };\n\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const displayValue = formatValue ? formatValue(value) : String(value);\n\n const handleClick = useCallback(() => {\n if (disabled || hasDragged.current) return;\n setEditValue(displayValue);\n setIsEditing(true);\n }, [disabled, displayValue]);\n\n const handleDivKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n if (/^[0-9]$/.test(e.key)) {\n e.preventDefault();\n setEditValue(e.key);\n setIsEditing(true);\n }\n },\n [disabled]\n );\n\n const commitEdit = useCallback(() => {\n const parsed = parseInt(editValue, 10);\n if (!isNaN(parsed)) {\n const clamped = Math.max(min, Math.min(max, parsed));\n onSet(clamped);\n }\n setIsEditing(false);\n }, [editValue, min, max, onSet]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') {\n commitEdit();\n } else if (e.key === 'Escape') {\n setIsEditing(false);\n }\n },\n [commitEdit]\n );\n\n if (isEditing) {\n return (\n <input\n type=\"text\"\n inputMode=\"numeric\"\n value={editValue}\n onChange={(e) => setEditValue(e.target.value.replace(/\\D/g, '').slice(-2))}\n onFocus={(e) => e.target.select()}\n onBlur={commitEdit}\n onKeyDown={handleKeyDown}\n className={`scrubtime-value scrubtime-value--editing ${className || ''}`}\n autoFocus\n />\n );\n }\n\n return (\n <div className=\"scrubtime-value-wrapper\">\n {showMagnifier && (\n <div className=\"scrubtime-magnifier\" aria-hidden=\"true\">\n {displayValue}\n </div>\n )}\n <div\n onMouseDown={handleMouseDown}\n onTouchStart={handleTouchStart}\n onClick={handleClick}\n onKeyDown={handleDivKeyDown}\n className={`scrubtime-value ${className || ''} ${disabled ? 'scrubtime-value--disabled' : ''}`}\n role=\"spinbutton\"\n aria-valuenow={value}\n aria-disabled={disabled}\n tabIndex={disabled ? -1 : 0}\n >\n {displayValue}\n </div>\n </div>\n );\n}\n\nfunction parseTime(time: string): { hours: number; minutes: number } {\n const [h, m] = time.split(':').map(Number);\n return { hours: h || 0, minutes: m || 0 };\n}\n\nfunction formatTime(hours: number, minutes: number): string {\n return `${hours}:${String(minutes).padStart(2, '0')}`;\n}\n\nfunction clampTotalMinutes(totalMins: number): number {\n return Math.max(0, Math.min(23 * 60 + 59, totalMins));\n}\n\nexport function TimePicker({\n value,\n onChange,\n label,\n className,\n disabled = false,\n sliderStep = 15,\n dragSensitivity = 3,\n divisions = 4,\n}: TimePickerProps) {\n const labelCount = divisions + 1;\n const { hours, minutes } = parseTime(value);\n const totalMinutes = hours * 60 + minutes;\n\n const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const mins = clampTotalMinutes(parseInt(e.target.value));\n const h = Math.floor(mins / 60);\n const m = mins % 60;\n onChange(formatTime(h, m));\n };\n\n const handleHoursDelta = useCallback(\n (delta: number) => {\n const newHours = Math.max(0, Math.min(23, hours + delta));\n onChange(formatTime(newHours, minutes));\n },\n [hours, minutes, onChange]\n );\n\n const handleHoursSet = useCallback(\n (newHours: number) => {\n onChange(formatTime(newHours, minutes));\n },\n [minutes, onChange]\n );\n\n const handleMinutesDelta = useCallback(\n (delta: number) => {\n const newTotalMinutes = clampTotalMinutes(totalMinutes + delta);\n const h = Math.floor(newTotalMinutes / 60);\n const m = newTotalMinutes % 60;\n onChange(formatTime(h, m));\n },\n [totalMinutes, onChange]\n );\n\n const handleMinutesSet = useCallback(\n (newMinutes: number) => {\n onChange(formatTime(hours, newMinutes));\n },\n [hours, onChange]\n );\n\n return (\n <div className={`scrubtime ${className || ''} ${disabled ? 'scrubtime--disabled' : ''}`}>\n {label && <label className=\"scrubtime-label\">{label}</label>}\n\n <div className=\"scrubtime-container\">\n <div className=\"scrubtime-display\">\n <DraggableValue\n value={hours}\n onDelta={handleHoursDelta}\n onSet={handleHoursSet}\n disabled={disabled}\n sensitivity={dragSensitivity * 2}\n min={0}\n max={23}\n className=\"scrubtime-hours\"\n />\n <span className=\"scrubtime-separator\">:</span>\n <DraggableValue\n value={minutes}\n onDelta={handleMinutesDelta}\n onSet={handleMinutesSet}\n formatValue={(v) => String(v).padStart(2, '0')}\n disabled={disabled}\n sensitivity={dragSensitivity}\n min={0}\n max={59}\n className=\"scrubtime-minutes\"\n />\n </div>\n\n <div className=\"scrubtime-slider-container\">\n <input\n type=\"range\"\n min={0}\n max={23 * 60 + 59}\n step={sliderStep}\n value={totalMinutes}\n onChange={handleSliderChange}\n disabled={disabled}\n tabIndex={-1}\n className=\"scrubtime-slider\"\n aria-label=\"Time slider\"\n />\n <div className=\"scrubtime-slider-labels\">\n {Array.from({ length: labelCount }, (_, i) => {\n const hour = Math.round((24 / divisions) * i);\n const percent = (i / divisions) * 100;\n return (\n <span key={i} style={{ left: `${percent}%` }}>\n {hour}\n </span>\n );\n })}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["e","touch","deltaX","deltaY"]}
|
package/dist/styles.css
CHANGED
|
@@ -63,6 +63,39 @@
|
|
|
63
63
|
gap: 0.25rem;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
.scrubtime-value-wrapper {
|
|
67
|
+
position: relative;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.scrubtime-magnifier {
|
|
71
|
+
position: absolute;
|
|
72
|
+
bottom: 100%;
|
|
73
|
+
left: 50%;
|
|
74
|
+
transform: translateX(-50%);
|
|
75
|
+
margin-bottom: 0.25rem;
|
|
76
|
+
background: var(--scrubtime-bg);
|
|
77
|
+
border: 2px solid var(--scrubtime-slider-thumb);
|
|
78
|
+
border-radius: var(--scrubtime-radius-sm);
|
|
79
|
+
padding: 0.4rem 0.6rem;
|
|
80
|
+
font-size: 1.1rem;
|
|
81
|
+
font-family: var(--scrubtime-font-mono);
|
|
82
|
+
color: var(--scrubtime-text);
|
|
83
|
+
white-space: nowrap;
|
|
84
|
+
pointer-events: none;
|
|
85
|
+
z-index: 1000;
|
|
86
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.scrubtime-magnifier::after {
|
|
90
|
+
content: '';
|
|
91
|
+
position: absolute;
|
|
92
|
+
top: 100%;
|
|
93
|
+
left: 50%;
|
|
94
|
+
transform: translateX(-50%);
|
|
95
|
+
border: 5px solid transparent;
|
|
96
|
+
border-top-color: var(--scrubtime-slider-thumb);
|
|
97
|
+
}
|
|
98
|
+
|
|
66
99
|
.scrubtime-value {
|
|
67
100
|
width: 3.5rem;
|
|
68
101
|
background: var(--scrubtime-value-bg);
|
|
@@ -116,6 +149,9 @@
|
|
|
116
149
|
display: flex;
|
|
117
150
|
flex-direction: column;
|
|
118
151
|
--thumb-width: 1.25rem;
|
|
152
|
+
padding: 0.75rem 0;
|
|
153
|
+
margin: -0.75rem 0;
|
|
154
|
+
user-select: none;
|
|
119
155
|
}
|
|
120
156
|
|
|
121
157
|
.scrubtime-slider {
|
|
@@ -128,6 +164,7 @@
|
|
|
128
164
|
cursor: pointer;
|
|
129
165
|
margin: 0;
|
|
130
166
|
touch-action: pan-x;
|
|
167
|
+
user-select: none;
|
|
131
168
|
}
|
|
132
169
|
|
|
133
170
|
.scrubtime-slider::-webkit-slider-thumb {
|
|
@@ -182,6 +219,8 @@
|
|
|
182
219
|
/* Inset to match thumb travel range */
|
|
183
220
|
margin-left: calc(var(--thumb-width) / 2);
|
|
184
221
|
margin-right: calc(var(--thumb-width) / 2 + 1px);
|
|
222
|
+
user-select: none;
|
|
223
|
+
pointer-events: none;
|
|
185
224
|
}
|
|
186
225
|
|
|
187
226
|
.scrubtime-slider-labels span {
|