scrubtime 0.2.0 → 0.3.1
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 +82 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +82 -25
- 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,13 @@ 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);
|
|
45
|
+
const enteredViaKeyboard = (0, import_react.useRef)(false);
|
|
44
46
|
const isDragging = (0, import_react.useRef)(false);
|
|
45
47
|
const hasDragged = (0, import_react.useRef)(false);
|
|
48
|
+
const isLongPress = (0, import_react.useRef)(false);
|
|
49
|
+
const needsBuffer = (0, import_react.useRef)(false);
|
|
50
|
+
const longPressTimer = (0, import_react.useRef)(null);
|
|
46
51
|
const startX = (0, import_react.useRef)(0);
|
|
47
52
|
const startY = (0, import_react.useRef)(0);
|
|
48
53
|
const lastX = (0, import_react.useRef)(0);
|
|
@@ -94,34 +99,76 @@ function DraggableValue({
|
|
|
94
99
|
const touch = e.touches[0];
|
|
95
100
|
isDragging.current = true;
|
|
96
101
|
hasDragged.current = false;
|
|
102
|
+
isLongPress.current = false;
|
|
97
103
|
startX.current = touch.clientX;
|
|
98
104
|
startY.current = touch.clientY;
|
|
99
105
|
lastX.current = touch.clientX;
|
|
100
106
|
lastY.current = touch.clientY;
|
|
101
107
|
accumulatedDelta.current = 0;
|
|
108
|
+
longPressTimer.current = setTimeout(() => {
|
|
109
|
+
isLongPress.current = true;
|
|
110
|
+
setShowMagnifier(true);
|
|
111
|
+
}, 300);
|
|
102
112
|
const handleTouchMove = (e2) => {
|
|
103
113
|
if (!isDragging.current) return;
|
|
104
114
|
const touch2 = e2.touches[0];
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
if (!hasDragged.current) {
|
|
116
|
+
const totalDeltaX = touch2.clientX - startX.current;
|
|
117
|
+
const totalDeltaY = touch2.clientY - startY.current;
|
|
118
|
+
if (isLongPress.current) {
|
|
119
|
+
const deltaX2 = touch2.clientX - lastX.current;
|
|
120
|
+
const deltaY2 = touch2.clientY - lastY.current;
|
|
121
|
+
if (Math.abs(deltaX2) > 0 || Math.abs(deltaY2) > 0) {
|
|
122
|
+
hasDragged.current = true;
|
|
123
|
+
e2.preventDefault();
|
|
124
|
+
const direction = deltaX2 + deltaY2 > 0 ? 1 : -1;
|
|
125
|
+
onDeltaRef.current(direction);
|
|
126
|
+
lastX.current = touch2.clientX;
|
|
127
|
+
lastY.current = touch2.clientY;
|
|
128
|
+
accumulatedDelta.current = -direction * 0.8;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (Math.abs(totalDeltaX) > DRAG_THRESHOLD || Math.abs(totalDeltaY) > DRAG_THRESHOLD) {
|
|
133
|
+
if (longPressTimer.current) {
|
|
134
|
+
clearTimeout(longPressTimer.current);
|
|
135
|
+
longPressTimer.current = null;
|
|
136
|
+
}
|
|
137
|
+
hasDragged.current = true;
|
|
138
|
+
needsBuffer.current = true;
|
|
139
|
+
setShowMagnifier(true);
|
|
140
|
+
e2.preventDefault();
|
|
141
|
+
lastX.current = touch2.clientX;
|
|
142
|
+
lastY.current = touch2.clientY;
|
|
143
|
+
accumulatedDelta.current = 0;
|
|
144
|
+
}
|
|
145
|
+
lastX.current = touch2.clientX;
|
|
146
|
+
lastY.current = touch2.clientY;
|
|
147
|
+
return;
|
|
110
148
|
}
|
|
149
|
+
e2.preventDefault();
|
|
111
150
|
const deltaX = touch2.clientX - lastX.current;
|
|
112
151
|
const deltaY = touch2.clientY - lastY.current;
|
|
113
|
-
const deltaValue = (deltaX
|
|
152
|
+
const deltaValue = (deltaX + deltaY) / sensitivity;
|
|
114
153
|
accumulatedDelta.current += deltaValue;
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
154
|
+
const threshold = needsBuffer.current ? 2 : 1;
|
|
155
|
+
if (Math.abs(accumulatedDelta.current) >= threshold) {
|
|
156
|
+
const wholeDelta = Math.trunc(accumulatedDelta.current);
|
|
117
157
|
accumulatedDelta.current -= wholeDelta;
|
|
118
158
|
onDeltaRef.current(wholeDelta);
|
|
159
|
+
needsBuffer.current = false;
|
|
119
160
|
}
|
|
120
161
|
lastX.current = touch2.clientX;
|
|
121
162
|
lastY.current = touch2.clientY;
|
|
122
163
|
};
|
|
123
164
|
const handleTouchEnd = () => {
|
|
124
165
|
isDragging.current = false;
|
|
166
|
+
needsBuffer.current = false;
|
|
167
|
+
if (longPressTimer.current) {
|
|
168
|
+
clearTimeout(longPressTimer.current);
|
|
169
|
+
longPressTimer.current = null;
|
|
170
|
+
}
|
|
171
|
+
setShowMagnifier(false);
|
|
125
172
|
document.removeEventListener("touchmove", handleTouchMove);
|
|
126
173
|
document.removeEventListener("touchend", handleTouchEnd);
|
|
127
174
|
};
|
|
@@ -133,6 +180,7 @@ function DraggableValue({
|
|
|
133
180
|
const displayValue = formatValue ? formatValue(value) : String(value);
|
|
134
181
|
const handleClick = (0, import_react.useCallback)(() => {
|
|
135
182
|
if (disabled || hasDragged.current) return;
|
|
183
|
+
enteredViaKeyboard.current = false;
|
|
136
184
|
setEditValue(displayValue);
|
|
137
185
|
setIsEditing(true);
|
|
138
186
|
}, [disabled, displayValue]);
|
|
@@ -141,6 +189,7 @@ function DraggableValue({
|
|
|
141
189
|
if (disabled) return;
|
|
142
190
|
if (/^[0-9]$/.test(e.key)) {
|
|
143
191
|
e.preventDefault();
|
|
192
|
+
enteredViaKeyboard.current = true;
|
|
144
193
|
setEditValue(e.key);
|
|
145
194
|
setIsEditing(true);
|
|
146
195
|
}
|
|
@@ -165,6 +214,11 @@ function DraggableValue({
|
|
|
165
214
|
},
|
|
166
215
|
[commitEdit]
|
|
167
216
|
);
|
|
217
|
+
const handleInputFocus = (0, import_react.useCallback)((e) => {
|
|
218
|
+
if (!enteredViaKeyboard.current) {
|
|
219
|
+
e.target.select();
|
|
220
|
+
}
|
|
221
|
+
}, []);
|
|
168
222
|
if (isEditing) {
|
|
169
223
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
170
224
|
"input",
|
|
@@ -173,7 +227,7 @@ function DraggableValue({
|
|
|
173
227
|
inputMode: "numeric",
|
|
174
228
|
value: editValue,
|
|
175
229
|
onChange: (e) => setEditValue(e.target.value.replace(/\D/g, "").slice(-2)),
|
|
176
|
-
onFocus:
|
|
230
|
+
onFocus: handleInputFocus,
|
|
177
231
|
onBlur: commitEdit,
|
|
178
232
|
onKeyDown: handleKeyDown,
|
|
179
233
|
className: `scrubtime-value scrubtime-value--editing ${className || ""}`,
|
|
@@ -181,21 +235,24 @@ function DraggableValue({
|
|
|
181
235
|
}
|
|
182
236
|
);
|
|
183
237
|
}
|
|
184
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.
|
|
185
|
-
"div",
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "scrubtime-value-wrapper", children: [
|
|
239
|
+
showMagnifier && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "scrubtime-magnifier", "aria-hidden": "true", children: displayValue }),
|
|
240
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
241
|
+
"div",
|
|
242
|
+
{
|
|
243
|
+
onMouseDown: handleMouseDown,
|
|
244
|
+
onTouchStart: handleTouchStart,
|
|
245
|
+
onClick: handleClick,
|
|
246
|
+
onKeyDown: handleDivKeyDown,
|
|
247
|
+
className: `scrubtime-value ${className || ""} ${disabled ? "scrubtime-value--disabled" : ""}`,
|
|
248
|
+
role: "spinbutton",
|
|
249
|
+
"aria-valuenow": value,
|
|
250
|
+
"aria-disabled": disabled,
|
|
251
|
+
tabIndex: disabled ? -1 : 0,
|
|
252
|
+
children: displayValue
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
] });
|
|
199
256
|
}
|
|
200
257
|
function parseTime(time) {
|
|
201
258
|
const [h, m] = time.split(":").map(Number);
|
|
@@ -281,7 +338,7 @@ function TimePicker({
|
|
|
281
338
|
onSet: handleMinutesSet,
|
|
282
339
|
formatValue: (v) => String(v).padStart(2, "0"),
|
|
283
340
|
disabled,
|
|
284
|
-
sensitivity: dragSensitivity
|
|
341
|
+
sensitivity: dragSensitivity,
|
|
285
342
|
min: 0,
|
|
286
343
|
max: 59,
|
|
287
344
|
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 enteredViaKeyboard = useRef(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 enteredViaKeyboard.current = false;\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 enteredViaKeyboard.current = true;\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 const handleInputFocus = useCallback((e: React.FocusEvent<HTMLInputElement>) => {\n if (!enteredViaKeyboard.current) {\n e.target.select();\n }\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={handleInputFocus}\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,13 @@ function DraggableValue({
|
|
|
15
15
|
}) {
|
|
16
16
|
const [isEditing, setIsEditing] = useState(false);
|
|
17
17
|
const [editValue, setEditValue] = useState("");
|
|
18
|
+
const [showMagnifier, setShowMagnifier] = useState(false);
|
|
19
|
+
const enteredViaKeyboard = useRef(false);
|
|
18
20
|
const isDragging = useRef(false);
|
|
19
21
|
const hasDragged = useRef(false);
|
|
22
|
+
const isLongPress = useRef(false);
|
|
23
|
+
const needsBuffer = useRef(false);
|
|
24
|
+
const longPressTimer = useRef(null);
|
|
20
25
|
const startX = useRef(0);
|
|
21
26
|
const startY = useRef(0);
|
|
22
27
|
const lastX = useRef(0);
|
|
@@ -68,34 +73,76 @@ function DraggableValue({
|
|
|
68
73
|
const touch = e.touches[0];
|
|
69
74
|
isDragging.current = true;
|
|
70
75
|
hasDragged.current = false;
|
|
76
|
+
isLongPress.current = false;
|
|
71
77
|
startX.current = touch.clientX;
|
|
72
78
|
startY.current = touch.clientY;
|
|
73
79
|
lastX.current = touch.clientX;
|
|
74
80
|
lastY.current = touch.clientY;
|
|
75
81
|
accumulatedDelta.current = 0;
|
|
82
|
+
longPressTimer.current = setTimeout(() => {
|
|
83
|
+
isLongPress.current = true;
|
|
84
|
+
setShowMagnifier(true);
|
|
85
|
+
}, 300);
|
|
76
86
|
const handleTouchMove = (e2) => {
|
|
77
87
|
if (!isDragging.current) return;
|
|
78
88
|
const touch2 = e2.touches[0];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
if (!hasDragged.current) {
|
|
90
|
+
const totalDeltaX = touch2.clientX - startX.current;
|
|
91
|
+
const totalDeltaY = touch2.clientY - startY.current;
|
|
92
|
+
if (isLongPress.current) {
|
|
93
|
+
const deltaX2 = touch2.clientX - lastX.current;
|
|
94
|
+
const deltaY2 = touch2.clientY - lastY.current;
|
|
95
|
+
if (Math.abs(deltaX2) > 0 || Math.abs(deltaY2) > 0) {
|
|
96
|
+
hasDragged.current = true;
|
|
97
|
+
e2.preventDefault();
|
|
98
|
+
const direction = deltaX2 + deltaY2 > 0 ? 1 : -1;
|
|
99
|
+
onDeltaRef.current(direction);
|
|
100
|
+
lastX.current = touch2.clientX;
|
|
101
|
+
lastY.current = touch2.clientY;
|
|
102
|
+
accumulatedDelta.current = -direction * 0.8;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (Math.abs(totalDeltaX) > DRAG_THRESHOLD || Math.abs(totalDeltaY) > DRAG_THRESHOLD) {
|
|
107
|
+
if (longPressTimer.current) {
|
|
108
|
+
clearTimeout(longPressTimer.current);
|
|
109
|
+
longPressTimer.current = null;
|
|
110
|
+
}
|
|
111
|
+
hasDragged.current = true;
|
|
112
|
+
needsBuffer.current = true;
|
|
113
|
+
setShowMagnifier(true);
|
|
114
|
+
e2.preventDefault();
|
|
115
|
+
lastX.current = touch2.clientX;
|
|
116
|
+
lastY.current = touch2.clientY;
|
|
117
|
+
accumulatedDelta.current = 0;
|
|
118
|
+
}
|
|
119
|
+
lastX.current = touch2.clientX;
|
|
120
|
+
lastY.current = touch2.clientY;
|
|
121
|
+
return;
|
|
84
122
|
}
|
|
123
|
+
e2.preventDefault();
|
|
85
124
|
const deltaX = touch2.clientX - lastX.current;
|
|
86
125
|
const deltaY = touch2.clientY - lastY.current;
|
|
87
|
-
const deltaValue = (deltaX
|
|
126
|
+
const deltaValue = (deltaX + deltaY) / sensitivity;
|
|
88
127
|
accumulatedDelta.current += deltaValue;
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
128
|
+
const threshold = needsBuffer.current ? 2 : 1;
|
|
129
|
+
if (Math.abs(accumulatedDelta.current) >= threshold) {
|
|
130
|
+
const wholeDelta = Math.trunc(accumulatedDelta.current);
|
|
91
131
|
accumulatedDelta.current -= wholeDelta;
|
|
92
132
|
onDeltaRef.current(wholeDelta);
|
|
133
|
+
needsBuffer.current = false;
|
|
93
134
|
}
|
|
94
135
|
lastX.current = touch2.clientX;
|
|
95
136
|
lastY.current = touch2.clientY;
|
|
96
137
|
};
|
|
97
138
|
const handleTouchEnd = () => {
|
|
98
139
|
isDragging.current = false;
|
|
140
|
+
needsBuffer.current = false;
|
|
141
|
+
if (longPressTimer.current) {
|
|
142
|
+
clearTimeout(longPressTimer.current);
|
|
143
|
+
longPressTimer.current = null;
|
|
144
|
+
}
|
|
145
|
+
setShowMagnifier(false);
|
|
99
146
|
document.removeEventListener("touchmove", handleTouchMove);
|
|
100
147
|
document.removeEventListener("touchend", handleTouchEnd);
|
|
101
148
|
};
|
|
@@ -107,6 +154,7 @@ function DraggableValue({
|
|
|
107
154
|
const displayValue = formatValue ? formatValue(value) : String(value);
|
|
108
155
|
const handleClick = useCallback(() => {
|
|
109
156
|
if (disabled || hasDragged.current) return;
|
|
157
|
+
enteredViaKeyboard.current = false;
|
|
110
158
|
setEditValue(displayValue);
|
|
111
159
|
setIsEditing(true);
|
|
112
160
|
}, [disabled, displayValue]);
|
|
@@ -115,6 +163,7 @@ function DraggableValue({
|
|
|
115
163
|
if (disabled) return;
|
|
116
164
|
if (/^[0-9]$/.test(e.key)) {
|
|
117
165
|
e.preventDefault();
|
|
166
|
+
enteredViaKeyboard.current = true;
|
|
118
167
|
setEditValue(e.key);
|
|
119
168
|
setIsEditing(true);
|
|
120
169
|
}
|
|
@@ -139,6 +188,11 @@ function DraggableValue({
|
|
|
139
188
|
},
|
|
140
189
|
[commitEdit]
|
|
141
190
|
);
|
|
191
|
+
const handleInputFocus = useCallback((e) => {
|
|
192
|
+
if (!enteredViaKeyboard.current) {
|
|
193
|
+
e.target.select();
|
|
194
|
+
}
|
|
195
|
+
}, []);
|
|
142
196
|
if (isEditing) {
|
|
143
197
|
return /* @__PURE__ */ jsx(
|
|
144
198
|
"input",
|
|
@@ -147,7 +201,7 @@ function DraggableValue({
|
|
|
147
201
|
inputMode: "numeric",
|
|
148
202
|
value: editValue,
|
|
149
203
|
onChange: (e) => setEditValue(e.target.value.replace(/\D/g, "").slice(-2)),
|
|
150
|
-
onFocus:
|
|
204
|
+
onFocus: handleInputFocus,
|
|
151
205
|
onBlur: commitEdit,
|
|
152
206
|
onKeyDown: handleKeyDown,
|
|
153
207
|
className: `scrubtime-value scrubtime-value--editing ${className || ""}`,
|
|
@@ -155,21 +209,24 @@ function DraggableValue({
|
|
|
155
209
|
}
|
|
156
210
|
);
|
|
157
211
|
}
|
|
158
|
-
return /* @__PURE__ */
|
|
159
|
-
"div",
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
212
|
+
return /* @__PURE__ */ jsxs("div", { className: "scrubtime-value-wrapper", children: [
|
|
213
|
+
showMagnifier && /* @__PURE__ */ jsx("div", { className: "scrubtime-magnifier", "aria-hidden": "true", children: displayValue }),
|
|
214
|
+
/* @__PURE__ */ jsx(
|
|
215
|
+
"div",
|
|
216
|
+
{
|
|
217
|
+
onMouseDown: handleMouseDown,
|
|
218
|
+
onTouchStart: handleTouchStart,
|
|
219
|
+
onClick: handleClick,
|
|
220
|
+
onKeyDown: handleDivKeyDown,
|
|
221
|
+
className: `scrubtime-value ${className || ""} ${disabled ? "scrubtime-value--disabled" : ""}`,
|
|
222
|
+
role: "spinbutton",
|
|
223
|
+
"aria-valuenow": value,
|
|
224
|
+
"aria-disabled": disabled,
|
|
225
|
+
tabIndex: disabled ? -1 : 0,
|
|
226
|
+
children: displayValue
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
] });
|
|
173
230
|
}
|
|
174
231
|
function parseTime(time) {
|
|
175
232
|
const [h, m] = time.split(":").map(Number);
|
|
@@ -255,7 +312,7 @@ function TimePicker({
|
|
|
255
312
|
onSet: handleMinutesSet,
|
|
256
313
|
formatValue: (v) => String(v).padStart(2, "0"),
|
|
257
314
|
disabled,
|
|
258
|
-
sensitivity: dragSensitivity
|
|
315
|
+
sensitivity: dragSensitivity,
|
|
259
316
|
min: 0,
|
|
260
317
|
max: 59,
|
|
261
318
|
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":"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","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 enteredViaKeyboard = useRef(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 enteredViaKeyboard.current = false;\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 enteredViaKeyboard.current = true;\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 const handleInputFocus = useCallback((e: React.FocusEvent<HTMLInputElement>) => {\n if (!enteredViaKeyboard.current) {\n e.target.select();\n }\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={handleInputFocus}\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":";AAAA,SAAS,QAAQ,aAAa,gBAAgB;AAyQxC,cAeF,YAfE;AAxON,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,CAAC,eAAe,gBAAgB,IAAI,SAAS,KAAK;AACxD,QAAM,qBAAqB,OAAO,KAAK;AACvC,QAAM,aAAa,OAAO,KAAK;AAC/B,QAAM,aAAa,OAAO,KAAK;AAC/B,QAAM,cAAc,OAAO,KAAK;AAChC,QAAM,cAAc,OAAO,KAAK;AAChC,QAAM,iBAAiB,OAA6C,IAAI;AACxE,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,kBAAY,UAAU;AACtB,aAAO,UAAU,MAAM;AACvB,aAAO,UAAU,MAAM;AACvB,YAAM,UAAU,MAAM;AACtB,YAAM,UAAU,MAAM;AACtB,uBAAiB,UAAU;AAG3B,qBAAe,UAAU,WAAW,MAAM;AACxC,oBAAY,UAAU;AACtB,yBAAiB,IAAI;AAAA,MACvB,GAAG,GAAG;AAEN,YAAM,kBAAkB,CAACA,OAAkB;AACzC,YAAI,CAAC,WAAW,QAAS;AAEzB,cAAMC,SAAQD,GAAE,QAAQ,CAAC;AAGzB,YAAI,CAAC,WAAW,SAAS;AACvB,gBAAM,cAAcC,OAAM,UAAU,OAAO;AAC3C,gBAAM,cAAcA,OAAM,UAAU,OAAO;AAG3C,cAAI,YAAY,SAAS;AACvB,kBAAMC,UAASD,OAAM,UAAU,MAAM;AACrC,kBAAME,UAASF,OAAM,UAAU,MAAM;AACrC,gBAAI,KAAK,IAAIC,OAAM,IAAI,KAAK,KAAK,IAAIC,OAAM,IAAI,GAAG;AAChD,yBAAW,UAAU;AACrB,cAAAH,GAAE,eAAe;AACjB,oBAAM,YAAaE,UAASC,UAAU,IAAI,IAAI;AAC9C,yBAAW,QAAQ,SAAS;AAC5B,oBAAM,UAAUF,OAAM;AACtB,oBAAM,UAAUA,OAAM;AAEtB,+BAAiB,UAAU,CAAC,YAAY;AACxC;AAAA,YACF;AAAA,UACF;AAEA,cAAI,KAAK,IAAI,WAAW,IAAI,kBAAkB,KAAK,IAAI,WAAW,IAAI,gBAAgB;AAEpF,gBAAI,eAAe,SAAS;AAC1B,2BAAa,eAAe,OAAO;AACnC,6BAAe,UAAU;AAAA,YAC3B;AACA,uBAAW,UAAU;AACrB,wBAAY,UAAU;AACtB,6BAAiB,IAAI;AACrB,YAAAD,GAAE,eAAe;AAEjB,kBAAM,UAAUC,OAAM;AACtB,kBAAM,UAAUA,OAAM;AACtB,6BAAiB,UAAU;AAAA,UAC7B;AAEA,gBAAM,UAAUA,OAAM;AACtB,gBAAM,UAAUA,OAAM;AACtB;AAAA,QACF;AAEA,QAAAD,GAAE,eAAe;AACjB,cAAM,SAASC,OAAM,UAAU,MAAM;AACrC,cAAM,SAASA,OAAM,UAAU,MAAM;AAGrC,cAAM,cAAc,SAAS,UAAU;AACvC,yBAAiB,WAAW;AAG5B,cAAM,YAAY,YAAY,UAAU,IAAI;AAC5C,YAAI,KAAK,IAAI,iBAAiB,OAAO,KAAK,WAAW;AACnD,gBAAM,aAAa,KAAK,MAAM,iBAAiB,OAAO;AACtD,2BAAiB,WAAW;AAC5B,qBAAW,QAAQ,UAAU;AAC7B,sBAAY,UAAU;AAAA,QACxB;AACA,cAAM,UAAUA,OAAM;AACtB,cAAM,UAAUA,OAAM;AAAA,MACxB;AAEA,YAAM,iBAAiB,MAAM;AAC3B,mBAAW,UAAU;AACrB,oBAAY,UAAU;AACtB,YAAI,eAAe,SAAS;AAC1B,uBAAa,eAAe,OAAO;AACnC,yBAAe,UAAU;AAAA,QAC3B;AACA,yBAAiB,KAAK;AACtB,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,uBAAmB,UAAU;AAC7B,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,2BAAmB,UAAU;AAC7B,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,QAAM,mBAAmB,YAAY,CAAC,MAA0C;AAC9E,QAAI,CAAC,mBAAmB,SAAS;AAC/B,QAAE,OAAO,OAAO;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,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;AAAA,QACT,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,WAAW,4CAA4C,aAAa,EAAE;AAAA,QACtE,WAAS;AAAA;AAAA,IACX;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,2BACZ;AAAA,qBACC,oBAAC,SAAI,WAAU,uBAAsB,eAAY,QAC9C,wBACH;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC,aAAa;AAAA,QACb,cAAc;AAAA,QACd,SAAS;AAAA,QACT,WAAW;AAAA,QACX,WAAW,mBAAmB,aAAa,EAAE,IAAI,WAAW,8BAA8B,EAAE;AAAA,QAC5F,MAAK;AAAA,QACL,iBAAe;AAAA,QACf,iBAAe;AAAA,QACf,UAAU,WAAW,KAAK;AAAA,QAEzB;AAAA;AAAA,IACH;AAAA,KACF;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;AAAA,YACb,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","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 {
|