@pixel-point/toolcraft 0.0.2

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.
Files changed (257) hide show
  1. package/LICENSE.md +98 -0
  2. package/README.md +41 -0
  3. package/bin/create-toolcraft-app.mjs +8 -0
  4. package/bin/toolcraft.mjs +8 -0
  5. package/package.json +24 -0
  6. package/scripts/prepare-pack.mjs +29 -0
  7. package/src/cli.mjs +392 -0
  8. package/src/cli.test.mjs +284 -0
  9. package/src/copy-recursive.mjs +86 -0
  10. package/src/generate.mjs +212 -0
  11. package/src/generate.test.mjs +322 -0
  12. package/src/import-map.mjs +14 -0
  13. package/src/package-json.mjs +80 -0
  14. package/src/package-json.test.mjs +67 -0
  15. package/src/rewrite-imports.mjs +85 -0
  16. package/src/rewrite-imports.test.mjs +58 -0
  17. package/templates/runtime/contracts/component-contracts.test.ts +1165 -0
  18. package/templates/runtime/contracts/component-contracts.ts +1340 -0
  19. package/templates/runtime/contracts/decision-contracts.test.ts +206 -0
  20. package/templates/runtime/contracts/decision-contracts.ts +283 -0
  21. package/templates/runtime/contracts/index.test.ts +14 -0
  22. package/templates/runtime/contracts/index.ts +3 -0
  23. package/templates/runtime/contracts/types.ts +56 -0
  24. package/templates/runtime/export/export.test.ts +203 -0
  25. package/templates/runtime/export/export.ts +132 -0
  26. package/templates/runtime/export/index.ts +1 -0
  27. package/templates/runtime/index.ts +14 -0
  28. package/templates/runtime/react/canvas-shell.test.tsx +424 -0
  29. package/templates/runtime/react/canvas-shell.tsx +408 -0
  30. package/templates/runtime/react/control-renderers.ts +31 -0
  31. package/templates/runtime/react/controls-panel.test.tsx +3736 -0
  32. package/templates/runtime/react/controls-panel.tsx +2327 -0
  33. package/templates/runtime/react/curve-geometry.test.ts +70 -0
  34. package/templates/runtime/react/index.ts +15 -0
  35. package/templates/runtime/react/layer-tree.ts +96 -0
  36. package/templates/runtime/react/layers-panel.test.tsx +487 -0
  37. package/templates/runtime/react/layers-panel.tsx +1348 -0
  38. package/templates/runtime/react/media-file.ts +82 -0
  39. package/templates/runtime/react/panel-host-config.ts +80 -0
  40. package/templates/runtime/react/panel-host-geometry.test.ts +66 -0
  41. package/templates/runtime/react/panel-host-geometry.ts +109 -0
  42. package/templates/runtime/react/panel-host-types.ts +74 -0
  43. package/templates/runtime/react/panel-host.test.tsx +102 -0
  44. package/templates/runtime/react/panel-host.tsx +353 -0
  45. package/templates/runtime/react/runtime-public-api.test.tsx +132 -0
  46. package/templates/runtime/react/settings-transfer.test.ts +150 -0
  47. package/templates/runtime/react/settings-transfer.ts +279 -0
  48. package/templates/runtime/react/storage-key-migration.ts +48 -0
  49. package/templates/runtime/react/theme-runtime.tsx +177 -0
  50. package/templates/runtime/react/timeline-panel.test.tsx +668 -0
  51. package/templates/runtime/react/timeline-panel.tsx +2953 -0
  52. package/templates/runtime/react/toolbar-panel.test.tsx +212 -0
  53. package/templates/runtime/react/toolbar-panel.tsx +205 -0
  54. package/templates/runtime/react/toolcraft-app.integration.test.tsx +350 -0
  55. package/templates/runtime/react/toolcraft-app.test.tsx +339 -0
  56. package/templates/runtime/react/toolcraft-app.tsx +81 -0
  57. package/templates/runtime/react/toolcraft-root.test.tsx +347 -0
  58. package/templates/runtime/react/toolcraft-root.tsx +203 -0
  59. package/templates/runtime/react/use-toolcraft.ts +41 -0
  60. package/templates/runtime/schema/define-toolcraft.test.ts +1524 -0
  61. package/templates/runtime/schema/define-toolcraft.ts +1442 -0
  62. package/templates/runtime/schema/keyframe-capability.test.ts +90 -0
  63. package/templates/runtime/schema/keyframe-capability.ts +51 -0
  64. package/templates/runtime/schema/runtime-targets.ts +40 -0
  65. package/templates/runtime/schema/types.ts +370 -0
  66. package/templates/runtime/state/canvas-zoom.ts +8 -0
  67. package/templates/runtime/state/create-template-state.test.ts +242 -0
  68. package/templates/runtime/state/create-template-state.ts +95 -0
  69. package/templates/runtime/state/keyframe-evaluation.test.ts +141 -0
  70. package/templates/runtime/state/keyframe-evaluation.ts +203 -0
  71. package/templates/runtime/state/persistence.test.ts +217 -0
  72. package/templates/runtime/state/persistence.ts +511 -0
  73. package/templates/runtime/state/reducer.test.ts +937 -0
  74. package/templates/runtime/state/reducer.ts +1212 -0
  75. package/templates/runtime/state/timeline-readiness.ts +43 -0
  76. package/templates/runtime/state/types.ts +242 -0
  77. package/templates/runtime/styles.css +125 -0
  78. package/templates/runtime/testing/performance.test.ts +1058 -0
  79. package/templates/runtime/testing/performance.ts +1078 -0
  80. package/templates/starter/AGENTS.md +186 -0
  81. package/templates/starter/LICENSE.md +98 -0
  82. package/templates/starter/NOTICE.md +8 -0
  83. package/templates/starter/docs/toolcraft/README.md +41 -0
  84. package/templates/starter/docs/toolcraft/acceptance-testing.md +205 -0
  85. package/templates/starter/docs/toolcraft/agent-worklog.md +81 -0
  86. package/templates/starter/docs/toolcraft/assembly-workflow.md +206 -0
  87. package/templates/starter/docs/toolcraft/component-rules.md +299 -0
  88. package/templates/starter/docs/toolcraft/custom-controls.md +71 -0
  89. package/templates/starter/docs/toolcraft/decision-contract.md +71 -0
  90. package/templates/starter/docs/toolcraft/performance.md +112 -0
  91. package/templates/starter/docs/toolcraft/renderer-technique.md +48 -0
  92. package/templates/starter/docs/toolcraft/schema-reference.md +265 -0
  93. package/templates/starter/docs/toolcraft/workflow.md +87 -0
  94. package/templates/starter/e2e/app-browser-acceptance.spec.ts +785 -0
  95. package/templates/starter/e2e/app-controls.spec.ts +41 -0
  96. package/templates/starter/e2e/app-performance.spec.ts +326 -0
  97. package/templates/starter/e2e/canvas-handle-helpers.ts +244 -0
  98. package/templates/starter/e2e/performance-helpers.ts +612 -0
  99. package/templates/starter/e2e/product-observable-helpers.ts +170 -0
  100. package/templates/starter/index.html +12 -0
  101. package/templates/starter/package.json +52 -0
  102. package/templates/starter/playwright.config.ts +43 -0
  103. package/templates/starter/scripts/check-ai-skills.mjs +95 -0
  104. package/templates/starter/scripts/check-toolcraft-docs.mjs +159 -0
  105. package/templates/starter/scripts/check-toolcraft-integrity.mjs +232 -0
  106. package/templates/starter/scripts/run-vite-on-free-port.mjs +48 -0
  107. package/templates/starter/scripts/toolcraft-port.mjs +54 -0
  108. package/templates/starter/scripts/toolcraft-port.test.mjs +73 -0
  109. package/templates/starter/src/app/starter-acceptance.test.ts +5959 -0
  110. package/templates/starter/src/app/starter-acceptance.ts +2646 -0
  111. package/templates/starter/src/app/starter-performance.test.ts +1390 -0
  112. package/templates/starter/src/app/starter-performance.ts +12 -0
  113. package/templates/starter/src/app/starter-schema.test.ts +70 -0
  114. package/templates/starter/src/app/starter-schema.ts +15 -0
  115. package/templates/starter/src/main.tsx +18 -0
  116. package/templates/starter/src/router.tsx +16 -0
  117. package/templates/starter/src/routes/index.tsx +7 -0
  118. package/templates/starter/src/routes/root.tsx +19 -0
  119. package/templates/starter/src/styles.css +120 -0
  120. package/templates/starter/tsconfig.json +11 -0
  121. package/templates/starter/vite.config.ts +13 -0
  122. package/templates/ui/components/composites/accordion.tsx +73 -0
  123. package/templates/ui/components/composites/alert-dialog.tsx +190 -0
  124. package/templates/ui/components/composites/alert.tsx +74 -0
  125. package/templates/ui/components/composites/aspect-ratio.tsx +22 -0
  126. package/templates/ui/components/composites/avatar.tsx +98 -0
  127. package/templates/ui/components/composites/badge.tsx +69 -0
  128. package/templates/ui/components/composites/breadcrumb.tsx +106 -0
  129. package/templates/ui/components/composites/card.tsx +91 -0
  130. package/templates/ui/components/composites/combobox.tsx +486 -0
  131. package/templates/ui/components/composites/command.tsx +296 -0
  132. package/templates/ui/components/composites/context-menu.tsx +247 -0
  133. package/templates/ui/components/composites/dialog.tsx +282 -0
  134. package/templates/ui/components/composites/dropdown-menu.tsx +299 -0
  135. package/templates/ui/components/composites/empty.tsx +110 -0
  136. package/templates/ui/components/composites/hover-card.tsx +44 -0
  137. package/templates/ui/components/composites/index.ts +30 -0
  138. package/templates/ui/components/composites/menubar.tsx +214 -0
  139. package/templates/ui/components/composites/navigation-menu.tsx +167 -0
  140. package/templates/ui/components/composites/pagination.tsx +131 -0
  141. package/templates/ui/components/composites/progress.tsx +72 -0
  142. package/templates/ui/components/composites/radio-group.tsx +84 -0
  143. package/templates/ui/components/composites/resizable.tsx +42 -0
  144. package/templates/ui/components/composites/sheet.tsx +153 -0
  145. package/templates/ui/components/composites/sidebar-structural.tsx +310 -0
  146. package/templates/ui/components/composites/sidebar.tsx +431 -0
  147. package/templates/ui/components/composites/sonner.tsx +35 -0
  148. package/templates/ui/components/composites/spinner.tsx +43 -0
  149. package/templates/ui/components/composites/table.tsx +108 -0
  150. package/templates/ui/components/composites/tabs.tsx +83 -0
  151. package/templates/ui/components/control-layout/index.tsx +437 -0
  152. package/templates/ui/components/controls/actions/actions-control.tsx +139 -0
  153. package/templates/ui/components/controls/actions/index.ts +9 -0
  154. package/templates/ui/components/controls/anchor-grid/anchor-grid-control.tsx +107 -0
  155. package/templates/ui/components/controls/anchor-grid/index.ts +4 -0
  156. package/templates/ui/components/controls/boolean/boolean-controls.tsx +79 -0
  157. package/templates/ui/components/controls/boolean/index.ts +4 -0
  158. package/templates/ui/components/controls/channel-mixer/channel-mixer-control.tsx +95 -0
  159. package/templates/ui/components/controls/channel-mixer/index.ts +4 -0
  160. package/templates/ui/components/controls/channel-tabs/channel-tabs.tsx +42 -0
  161. package/templates/ui/components/controls/channel-tabs/index.ts +6 -0
  162. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +90 -0
  163. package/templates/ui/components/controls/code-textarea/index.ts +4 -0
  164. package/templates/ui/components/controls/color/color-control.tsx +571 -0
  165. package/templates/ui/components/controls/color/color-picker-popover.tsx +104 -0
  166. package/templates/ui/components/controls/color/index.ts +41 -0
  167. package/templates/ui/components/controls/color/palette-control-data.ts +436 -0
  168. package/templates/ui/components/controls/color/palette-control.tsx +535 -0
  169. package/templates/ui/components/controls/color/style-guide-color-picker-channel-utils.ts +162 -0
  170. package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +190 -0
  171. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +485 -0
  172. package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +710 -0
  173. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +503 -0
  174. package/templates/ui/components/controls/control-types.ts +43 -0
  175. package/templates/ui/components/controls/curves/curve-geometry.ts +355 -0
  176. package/templates/ui/components/controls/curves/curve-graph.tsx +390 -0
  177. package/templates/ui/components/controls/curves/curves-control.tsx +445 -0
  178. package/templates/ui/components/controls/curves/index.ts +6 -0
  179. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +191 -0
  180. package/templates/ui/components/controls/file-drop/index.ts +5 -0
  181. package/templates/ui/components/controls/font-picker/font-catalog.json +15360 -0
  182. package/templates/ui/components/controls/font-picker/font-catalog.ts +116 -0
  183. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1202 -0
  184. package/templates/ui/components/controls/font-picker/font-preview-loader.ts +336 -0
  185. package/templates/ui/components/controls/font-picker/index.ts +24 -0
  186. package/templates/ui/components/controls/font-picker/use-hover-intent.ts +46 -0
  187. package/templates/ui/components/controls/gradient/gradient-control-utils.ts +190 -0
  188. package/templates/ui/components/controls/gradient/gradient-control.tsx +612 -0
  189. package/templates/ui/components/controls/gradient/gradient-stop-list.tsx +400 -0
  190. package/templates/ui/components/controls/gradient/gradient-toolbar.tsx +152 -0
  191. package/templates/ui/components/controls/gradient/index.ts +4 -0
  192. package/templates/ui/components/controls/image-picker/image-picker-control.tsx +139 -0
  193. package/templates/ui/components/controls/image-picker/index.ts +7 -0
  194. package/templates/ui/components/controls/index.ts +192 -0
  195. package/templates/ui/components/controls/range-input/index.ts +4 -0
  196. package/templates/ui/components/controls/range-input/range-input-control.tsx +173 -0
  197. package/templates/ui/components/controls/range-slider/index.ts +4 -0
  198. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +122 -0
  199. package/templates/ui/components/controls/range-slider/range-slider-value.ts +61 -0
  200. package/templates/ui/components/controls/segmented/index.ts +8 -0
  201. package/templates/ui/components/controls/segmented/segmented-control.tsx +94 -0
  202. package/templates/ui/components/controls/select/index.ts +4 -0
  203. package/templates/ui/components/controls/select/select-control.tsx +223 -0
  204. package/templates/ui/components/controls/slider/index.ts +4 -0
  205. package/templates/ui/components/controls/slider/slider-control.tsx +150 -0
  206. package/templates/ui/components/controls/slider/slider-value.ts +56 -0
  207. package/templates/ui/components/controls/text-input/index.ts +4 -0
  208. package/templates/ui/components/controls/text-input/text-input-control.tsx +158 -0
  209. package/templates/ui/components/controls/use-measured-element-width.ts +42 -0
  210. package/templates/ui/components/controls/vector/index.ts +8 -0
  211. package/templates/ui/components/controls/vector/vector-control.tsx +401 -0
  212. package/templates/ui/components/panel/index.ts +19 -0
  213. package/templates/ui/components/panel/panel-actions.tsx +165 -0
  214. package/templates/ui/components/panel/panel-header.tsx +61 -0
  215. package/templates/ui/components/panel/panel-icon-button.tsx +96 -0
  216. package/templates/ui/components/panel/panel-section.tsx +168 -0
  217. package/templates/ui/components/panel/panel-surface.tsx +206 -0
  218. package/templates/ui/components/panel/panel.tsx +210 -0
  219. package/templates/ui/components/primitives/animated-loader.tsx +61 -0
  220. package/templates/ui/components/primitives/button-group.tsx +134 -0
  221. package/templates/ui/components/primitives/button.tsx +429 -0
  222. package/templates/ui/components/primitives/checkbox.tsx +62 -0
  223. package/templates/ui/components/primitives/editable-slider-value-label.tsx +337 -0
  224. package/templates/ui/components/primitives/field.tsx +225 -0
  225. package/templates/ui/components/primitives/index.ts +82 -0
  226. package/templates/ui/components/primitives/input-group.tsx +298 -0
  227. package/templates/ui/components/primitives/input.tsx +61 -0
  228. package/templates/ui/components/primitives/internal/button-loading.tsx +178 -0
  229. package/templates/ui/components/primitives/label.tsx +16 -0
  230. package/templates/ui/components/primitives/popover.tsx +126 -0
  231. package/templates/ui/components/primitives/portal-layer-context.tsx +33 -0
  232. package/templates/ui/components/primitives/primitive-arrow-icon.tsx +38 -0
  233. package/templates/ui/components/primitives/scroll-fade-logic.ts +441 -0
  234. package/templates/ui/components/primitives/scroll-fade-render.tsx +75 -0
  235. package/templates/ui/components/primitives/scroll-fade-types.ts +41 -0
  236. package/templates/ui/components/primitives/scroll-fade.tsx +72 -0
  237. package/templates/ui/components/primitives/select.tsx +408 -0
  238. package/templates/ui/components/primitives/selection-state.ts +31 -0
  239. package/templates/ui/components/primitives/separator.tsx +21 -0
  240. package/templates/ui/components/primitives/slider/index.ts +4 -0
  241. package/templates/ui/components/primitives/slider/slider-interaction.tsx +96 -0
  242. package/templates/ui/components/primitives/slider/slider-parts.tsx +303 -0
  243. package/templates/ui/components/primitives/slider/slider-reset.ts +152 -0
  244. package/templates/ui/components/primitives/slider/slider-value.ts +114 -0
  245. package/templates/ui/components/primitives/slider/slider.tsx +511 -0
  246. package/templates/ui/components/primitives/switch.tsx +35 -0
  247. package/templates/ui/components/primitives/textarea.tsx +49 -0
  248. package/templates/ui/components/primitives/toggle-group.tsx +114 -0
  249. package/templates/ui/components/primitives/toggle.tsx +46 -0
  250. package/templates/ui/components/primitives/tooltip.tsx +100 -0
  251. package/templates/ui/hooks/use-mobile.ts +21 -0
  252. package/templates/ui/index.ts +31 -0
  253. package/templates/ui/lib/control-outline.ts +3 -0
  254. package/templates/ui/lib/input-control-style.ts +131 -0
  255. package/templates/ui/lib/style-guide-color-utils.ts +111 -0
  256. package/templates/ui/lib/utils.ts +6 -0
  257. package/templates/ui/styles.css +291 -0
@@ -0,0 +1,303 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Slider as SliderPrimitive } from "@base-ui/react/slider";
5
+
6
+ import { cn } from "../../../lib/utils";
7
+ import type { SliderThumbDoubleClickHandler } from "./slider-reset";
8
+
9
+ type SliderOrientation = "horizontal" | "vertical";
10
+
11
+ type SliderControlContentProps = {
12
+ count: number;
13
+ disabled?: boolean;
14
+ getAriaLabel?: (index: number) => string;
15
+ isDiscrete: boolean;
16
+ isPointerDragging: boolean;
17
+ markerCount: number;
18
+ markerValues?: readonly number[];
19
+ max: number;
20
+ min: number;
21
+ onThumbDoubleClick?: SliderThumbDoubleClickHandler;
22
+ orientation: SliderOrientation;
23
+ showFill: boolean;
24
+ };
25
+
26
+ type SliderThumbPointerSnapshot = {
27
+ clientX: number;
28
+ clientY: number;
29
+ index: number;
30
+ pointerType: string;
31
+ timeStamp: number;
32
+ };
33
+
34
+ const sliderThumbDoubleClickDelayMs = 340;
35
+ const sliderThumbDoubleClickDistancePx = 6;
36
+
37
+ function useActiveSliderThumbDrag(disabled: boolean): {
38
+ activeDragIndex: number | null;
39
+ handlePointerDown: (event: React.PointerEvent<HTMLDivElement>, index: number) => void;
40
+ } {
41
+ const [activeDragIndex, setActiveDragIndex] = React.useState<number | null>(null);
42
+ const clearActiveDragIndex = React.useCallback(() => {
43
+ setActiveDragIndex(null);
44
+ }, []);
45
+ const handlePointerDown = React.useCallback(
46
+ (event: React.PointerEvent<HTMLDivElement>, index: number) => {
47
+ if (event.defaultPrevented || disabled || event.button !== 0) {
48
+ return;
49
+ }
50
+
51
+ setActiveDragIndex(index);
52
+ },
53
+ [disabled],
54
+ );
55
+
56
+ React.useEffect(() => {
57
+ if (activeDragIndex === null) {
58
+ return undefined;
59
+ }
60
+
61
+ window.addEventListener("pointerup", clearActiveDragIndex);
62
+ window.addEventListener("pointercancel", clearActiveDragIndex);
63
+ window.addEventListener("blur", clearActiveDragIndex);
64
+
65
+ return () => {
66
+ window.removeEventListener("pointerup", clearActiveDragIndex);
67
+ window.removeEventListener("pointercancel", clearActiveDragIndex);
68
+ window.removeEventListener("blur", clearActiveDragIndex);
69
+ };
70
+ }, [activeDragIndex, clearActiveDragIndex]);
71
+
72
+ return { activeDragIndex, handlePointerDown };
73
+ }
74
+
75
+ function SliderMarkers({
76
+ disabled = false,
77
+ isPointerDragging,
78
+ markerCount,
79
+ markerValues,
80
+ max,
81
+ min,
82
+ orientation,
83
+ }: {
84
+ disabled?: boolean;
85
+ isPointerDragging: boolean;
86
+ markerCount: number;
87
+ markerValues?: readonly number[];
88
+ max: number;
89
+ min: number;
90
+ orientation: SliderOrientation;
91
+ }): React.JSX.Element | null {
92
+ const markerOffsets = markerValues?.length
93
+ ? markerValues
94
+ .filter((value) => value > min && value < max)
95
+ .map((value) => ((value - min) / (max - min)) * 100)
96
+ : Array.from({ length: markerCount }, (_, index) => {
97
+ if (index === 0 || index === markerCount - 1) {
98
+ return null;
99
+ }
100
+
101
+ return (index / (markerCount - 1)) * 100;
102
+ }).filter((offset): offset is number => offset !== null);
103
+
104
+ if (markerOffsets.length === 0) {
105
+ return null;
106
+ }
107
+
108
+ return (
109
+ <>
110
+ {markerOffsets.map((offsetValue, index) => {
111
+ const offset = `${offsetValue}%`;
112
+ return (
113
+ <span
114
+ aria-hidden
115
+ className={cn(
116
+ "pointer-events-none absolute rounded-full bg-[color:var(--slider-active-color)] opacity-0 transition-opacity duration-150",
117
+ disabled
118
+ ? null
119
+ : isPointerDragging
120
+ ? "opacity-100"
121
+ : "group-hover/slider-control:opacity-100",
122
+ orientation === "vertical"
123
+ ? "left-1/2 h-px w-1.5 -translate-x-1/2 translate-y-1/2"
124
+ : "top-1/2 h-1.5 w-px -translate-x-1/2 -translate-y-1/2",
125
+ )}
126
+ data-slot="slider-marker"
127
+ key={`${offset}-${index}`}
128
+ style={orientation === "vertical" ? { bottom: offset } : { left: offset }}
129
+ />
130
+ );
131
+ })}
132
+ </>
133
+ );
134
+ }
135
+
136
+ function SliderThumbs({
137
+ count,
138
+ disabled = false,
139
+ getAriaLabel,
140
+ onDoubleClick,
141
+ }: {
142
+ count: number;
143
+ disabled?: boolean;
144
+ getAriaLabel?: (index: number) => string;
145
+ onDoubleClick?: SliderThumbDoubleClickHandler;
146
+ }): React.JSX.Element {
147
+ const { activeDragIndex, handlePointerDown } = useActiveSliderThumbDrag(disabled);
148
+ const lastPointerDownRef = React.useRef<SliderThumbPointerSnapshot | null>(null);
149
+ const suppressNextDoubleClickRef = React.useRef(false);
150
+
151
+ const shouldResetFromPointerDown = React.useCallback(
152
+ (event: React.PointerEvent<HTMLDivElement>, index: number): boolean => {
153
+ if (event.detail >= 2) {
154
+ return true;
155
+ }
156
+
157
+ const previousPointerDown = lastPointerDownRef.current;
158
+ if (!previousPointerDown || previousPointerDown.index !== index) {
159
+ return false;
160
+ }
161
+
162
+ const deltaTime = event.timeStamp - previousPointerDown.timeStamp;
163
+ const deltaX = event.clientX - previousPointerDown.clientX;
164
+ const deltaY = event.clientY - previousPointerDown.clientY;
165
+ const distance = Math.hypot(deltaX, deltaY);
166
+
167
+ return (
168
+ previousPointerDown.pointerType === event.pointerType &&
169
+ deltaTime >= 0 &&
170
+ deltaTime <= sliderThumbDoubleClickDelayMs &&
171
+ distance <= sliderThumbDoubleClickDistancePx
172
+ );
173
+ },
174
+ [],
175
+ );
176
+ const handlePointerDownCapture = React.useCallback(
177
+ (event: React.PointerEvent<HTMLDivElement>, index: number) => {
178
+ if (!onDoubleClick || disabled || event.button !== 0) {
179
+ return;
180
+ }
181
+
182
+ if (shouldResetFromPointerDown(event, index)) {
183
+ lastPointerDownRef.current = null;
184
+ suppressNextDoubleClickRef.current = true;
185
+ onDoubleClick(event, index);
186
+ return;
187
+ }
188
+
189
+ lastPointerDownRef.current = {
190
+ clientX: event.clientX,
191
+ clientY: event.clientY,
192
+ index,
193
+ pointerType: event.pointerType,
194
+ timeStamp: event.timeStamp,
195
+ };
196
+ },
197
+ [disabled, onDoubleClick, shouldResetFromPointerDown],
198
+ );
199
+ const handleDoubleClick = React.useCallback(
200
+ (event: React.MouseEvent<HTMLDivElement>, index: number) => {
201
+ if (suppressNextDoubleClickRef.current) {
202
+ suppressNextDoubleClickRef.current = false;
203
+ event.preventDefault();
204
+ event.stopPropagation();
205
+ return;
206
+ }
207
+
208
+ onDoubleClick?.(event, index);
209
+ },
210
+ [onDoubleClick],
211
+ );
212
+
213
+ return (
214
+ <>
215
+ {Array.from({ length: count }, (_, index) => (
216
+ <SliderPrimitive.Thumb
217
+ data-slot="slider-thumb"
218
+ getAriaLabel={getAriaLabel}
219
+ index={index}
220
+ key={index}
221
+ onDoubleClick={(event) => handleDoubleClick(event, index)}
222
+ onPointerDownCapture={(event) => handlePointerDownCapture(event, index)}
223
+ onPointerDown={(event) => handlePointerDown(event, index)}
224
+ className={cn(
225
+ "group/slider-thumb relative block size-[9px] shrink-0 cursor-pointer rounded-[2px] select-none before:absolute before:top-1/2 before:left-1/2 before:block before:size-[18px] before:-translate-x-1/2 before:-translate-y-1/2 before:content-[''] transition-[inset-inline-start,bottom] duration-200 ease-out data-[dragging]:transition-none disabled:pointer-events-none motion-reduce:transition-none",
226
+ )}
227
+ >
228
+ <span
229
+ aria-hidden
230
+ data-active-dragging={activeDragIndex === index ? "true" : undefined}
231
+ data-slot="slider-dot"
232
+ className={cn(
233
+ "pointer-events-none absolute inset-0 block rounded-[2px] bg-[color:var(--slider-active-color)] transition-[scale,background-color] duration-200 ease-out motion-reduce:transition-none",
234
+ disabled
235
+ ? null
236
+ : "group-hover/slider-thumb:scale-[1.4] data-[active-dragging=true]:scale-[1.4]",
237
+ )}
238
+ />
239
+ </SliderPrimitive.Thumb>
240
+ ))}
241
+ </>
242
+ );
243
+ }
244
+
245
+ function SliderFill({ show }: { show: boolean }): React.JSX.Element | null {
246
+ if (!show) {
247
+ return null;
248
+ }
249
+
250
+ return (
251
+ <SliderPrimitive.Indicator
252
+ data-slot="slider-range"
253
+ className={cn(
254
+ "bg-[color:var(--slider-active-color)] transition-[width,height,inset-inline-start,bottom] duration-200 ease-out select-none data-[dragging]:transition-none data-horizontal:h-full data-vertical:w-full motion-reduce:transition-none",
255
+ )}
256
+ />
257
+ );
258
+ }
259
+
260
+ function SliderControlContent({
261
+ count,
262
+ disabled,
263
+ getAriaLabel,
264
+ isDiscrete,
265
+ isPointerDragging,
266
+ markerCount,
267
+ markerValues,
268
+ max,
269
+ min,
270
+ onThumbDoubleClick,
271
+ orientation,
272
+ showFill,
273
+ }: SliderControlContentProps): React.JSX.Element {
274
+ return (
275
+ <SliderPrimitive.Control className="group/slider-control relative flex touch-none items-center select-none data-[disabled]:opacity-[0.15] data-horizontal:h-[18px] data-horizontal:w-full data-vertical:h-full data-vertical:min-h-40 data-vertical:w-[18px] data-vertical:flex-col">
276
+ <SliderPrimitive.Track
277
+ data-slot="slider-track"
278
+ className="group/slider-track relative grow overflow-visible rounded-full bg-[color:var(--slider-track-color)] select-none data-horizontal:h-px data-horizontal:w-full data-vertical:h-full data-vertical:w-px"
279
+ >
280
+ <SliderFill show={showFill} />
281
+ {isDiscrete ? (
282
+ <SliderMarkers
283
+ disabled={disabled}
284
+ isPointerDragging={isPointerDragging}
285
+ markerCount={markerCount}
286
+ markerValues={markerValues}
287
+ max={max}
288
+ min={min}
289
+ orientation={orientation}
290
+ />
291
+ ) : null}
292
+ </SliderPrimitive.Track>
293
+ <SliderThumbs
294
+ count={count}
295
+ disabled={disabled}
296
+ getAriaLabel={getAriaLabel}
297
+ onDoubleClick={onThumbDoubleClick}
298
+ />
299
+ </SliderPrimitive.Control>
300
+ );
301
+ }
302
+
303
+ export { SliderControlContent, SliderFill, SliderMarkers, SliderThumbs };
@@ -0,0 +1,152 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import type { Slider as SliderPrimitive } from "@base-ui/react/slider";
5
+
6
+ import {
7
+ normalizeSliderValueShape,
8
+ snapSliderValue,
9
+ valuesMatch,
10
+ type SliderRuntimeValue,
11
+ type SliderValue,
12
+ } from "./slider-value";
13
+
14
+ export type SliderThumbDoubleClickHandler = (
15
+ event: React.MouseEvent<HTMLDivElement> | React.PointerEvent<HTMLDivElement>,
16
+ index: number,
17
+ ) => void;
18
+
19
+ type UseSliderThumbResetOptions<Value extends number | readonly number[]> = {
20
+ defaultValue?: Value;
21
+ disabled?: boolean;
22
+ handleValueChange: SliderPrimitive.Root.Props<Value>["onValueChange"];
23
+ handleValueCommitted: (
24
+ value: SliderValue<Value>,
25
+ eventDetails: SliderPrimitive.Root.CommitEventDetails,
26
+ ) => void;
27
+ isDiscrete: boolean;
28
+ max: number;
29
+ min: number;
30
+ resetValue?: Value;
31
+ snapValues?: readonly number[];
32
+ step: number;
33
+ value?: Value;
34
+ values: readonly number[];
35
+ };
36
+
37
+ function createSliderResetChangeEventDetails(
38
+ event: React.MouseEvent<HTMLDivElement> | React.PointerEvent<HTMLDivElement>,
39
+ activeThumbIndex: number,
40
+ ): SliderPrimitive.Root.ChangeEventDetails {
41
+ return {
42
+ activeThumbIndex,
43
+ allowPropagation: () => undefined,
44
+ cancel: () => undefined,
45
+ event: event.nativeEvent,
46
+ isCanceled: false,
47
+ isPropagationAllowed: false,
48
+ reason: "none",
49
+ trigger: event.currentTarget,
50
+ };
51
+ }
52
+
53
+ function createSliderResetCommitEventDetails(
54
+ event: React.MouseEvent<HTMLDivElement> | React.PointerEvent<HTMLDivElement>,
55
+ ): SliderPrimitive.Root.CommitEventDetails {
56
+ return {
57
+ event: event.nativeEvent,
58
+ reason: "none",
59
+ };
60
+ }
61
+
62
+ function getThumbResetValue<Value extends number | readonly number[]>(
63
+ currentValue: Value,
64
+ resetValue: Value,
65
+ index: number,
66
+ ): Value {
67
+ if (!Array.isArray(currentValue) || !Array.isArray(resetValue)) {
68
+ return resetValue;
69
+ }
70
+
71
+ const nextValue = [...currentValue];
72
+ const nextThumbValue = resetValue[index] ?? currentValue[index];
73
+ if (nextThumbValue === undefined) {
74
+ return currentValue;
75
+ }
76
+
77
+ nextValue[index] = nextThumbValue;
78
+
79
+ return nextValue as unknown as Value;
80
+ }
81
+
82
+ export function useSliderThumbReset<Value extends number | readonly number[]>({
83
+ defaultValue,
84
+ disabled,
85
+ handleValueChange,
86
+ handleValueCommitted,
87
+ isDiscrete,
88
+ max,
89
+ min,
90
+ resetValue,
91
+ snapValues,
92
+ step,
93
+ value,
94
+ values,
95
+ }: UseSliderThumbResetOptions<Value>): SliderThumbDoubleClickHandler {
96
+ const initialResetValueRef = React.useRef<Value | undefined>(resetValue ?? defaultValue ?? value);
97
+
98
+ return React.useCallback(
99
+ (event, index) => {
100
+ if (disabled) {
101
+ return;
102
+ }
103
+
104
+ const resetTarget = resetValue ?? defaultValue ?? initialResetValueRef.current;
105
+ if (resetTarget === undefined) {
106
+ return;
107
+ }
108
+
109
+ event.preventDefault();
110
+ event.stopPropagation();
111
+
112
+ const normalizedResetValue = normalizeSliderValueShape(
113
+ resetTarget as SliderRuntimeValue,
114
+ value,
115
+ defaultValue,
116
+ min,
117
+ );
118
+ const currentValue = normalizeSliderValueShape(values, value, defaultValue, min);
119
+ const resetThumbValue = getThumbResetValue(currentValue, normalizedResetValue, index);
120
+ const nextValue = isDiscrete
121
+ ? snapSliderValue(resetThumbValue, min, max, step, snapValues)
122
+ : resetThumbValue;
123
+
124
+ if (valuesMatch(currentValue, nextValue)) {
125
+ return;
126
+ }
127
+
128
+ handleValueChange?.(
129
+ nextValue as SliderValue<Value>,
130
+ createSliderResetChangeEventDetails(event, index),
131
+ );
132
+ handleValueCommitted(
133
+ nextValue as SliderValue<Value>,
134
+ createSliderResetCommitEventDetails(event),
135
+ );
136
+ },
137
+ [
138
+ defaultValue,
139
+ disabled,
140
+ handleValueChange,
141
+ handleValueCommitted,
142
+ isDiscrete,
143
+ max,
144
+ min,
145
+ resetValue,
146
+ snapValues,
147
+ step,
148
+ value,
149
+ values,
150
+ ],
151
+ );
152
+ }
@@ -0,0 +1,114 @@
1
+ export type SliderRuntimeValue = number | readonly number[];
2
+ export type SliderValue<Value extends number | readonly number[]> = Value extends number
3
+ ? number
4
+ : Value;
5
+
6
+ function snapValue(value: number, min: number, max: number, step: number): number {
7
+ const safeStep = step > 0 ? step : 1;
8
+ const snapped = min + Math.round((value - min) / safeStep) * safeStep;
9
+
10
+ return Math.min(max, Math.max(min, Number(snapped.toFixed(6))));
11
+ }
12
+
13
+ function snapValueToAllowedValues(
14
+ value: number,
15
+ min: number,
16
+ max: number,
17
+ allowedValues: readonly number[] | undefined,
18
+ ): number | null {
19
+ if (!allowedValues?.length) {
20
+ return null;
21
+ }
22
+
23
+ const safeValues = allowedValues
24
+ .filter((item) => Number.isFinite(item))
25
+ .map((item) => Math.min(max, Math.max(min, item)));
26
+
27
+ if (!safeValues.length) {
28
+ return null;
29
+ }
30
+
31
+ const nearestValue = safeValues.reduce((nearest, item) => {
32
+ const nearestDistance = Math.abs(nearest - value);
33
+ const itemDistance = Math.abs(item - value);
34
+
35
+ return itemDistance < nearestDistance ? item : nearest;
36
+ }, safeValues[0]!);
37
+
38
+ return Number(nearestValue.toFixed(6));
39
+ }
40
+
41
+ export function getSliderValues<Value extends number | readonly number[]>(
42
+ value: Value | undefined,
43
+ defaultValue: Value | undefined,
44
+ min: number,
45
+ max: number,
46
+ ): number[] {
47
+ if (Array.isArray(value)) {
48
+ return [...value];
49
+ }
50
+
51
+ if (typeof value === "number") {
52
+ return [value];
53
+ }
54
+
55
+ if (Array.isArray(defaultValue)) {
56
+ return [...defaultValue];
57
+ }
58
+
59
+ if (typeof defaultValue === "number") {
60
+ return [defaultValue];
61
+ }
62
+
63
+ return [min, max];
64
+ }
65
+
66
+ export function normalizeSliderValueShape<Value extends number | readonly number[]>(
67
+ nextValue: SliderRuntimeValue,
68
+ value: Value | undefined,
69
+ defaultValue: Value | undefined,
70
+ min: number,
71
+ ): Value {
72
+ const referenceValue = value ?? defaultValue;
73
+
74
+ if (Array.isArray(referenceValue)) {
75
+ return (Array.isArray(nextValue) ? [...nextValue] : [nextValue]) as unknown as Value;
76
+ }
77
+
78
+ if (typeof referenceValue === "number") {
79
+ return (Array.isArray(nextValue) ? (nextValue[0] ?? min) : nextValue) as Value;
80
+ }
81
+
82
+ return nextValue as Value;
83
+ }
84
+
85
+ export function snapSliderValue<Value extends number | readonly number[]>(
86
+ nextValue: Value,
87
+ min: number,
88
+ max: number,
89
+ step: number,
90
+ snapValues?: readonly number[],
91
+ ): Value {
92
+ if (typeof nextValue === "number") {
93
+ const allowedValue = snapValueToAllowedValues(nextValue, min, max, snapValues);
94
+ if (allowedValue !== null) {
95
+ return allowedValue as Value;
96
+ }
97
+
98
+ return snapValue(nextValue, min, max, step) as Value;
99
+ }
100
+
101
+ return nextValue.map((item) => (
102
+ snapValueToAllowedValues(item, min, max, snapValues) ?? snapValue(item, min, max, step)
103
+ )) as unknown as Value;
104
+ }
105
+
106
+ export function valuesMatch<Value extends number | readonly number[]>(left: Value, right: Value): boolean {
107
+ if (Array.isArray(left) && Array.isArray(right)) {
108
+ return (
109
+ left.length === right.length && left.every((leftValue, index) => leftValue === right[index])
110
+ );
111
+ }
112
+
113
+ return left === right;
114
+ }