@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,612 @@
1
+ "use client";
2
+
3
+ import {
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ type Dispatch,
8
+ type MutableRefObject,
9
+ type RefObject,
10
+ type SetStateAction,
11
+ } from "react";
12
+
13
+ import { Field } from "../../primitives";
14
+ import { selectedItemBorderClassName } from "../../primitives/selection-state";
15
+ import {
16
+ createControlHistoryGroupId,
17
+ type ControlChangeMeta,
18
+ type ControlValueChangeHandler,
19
+ type GradientStop,
20
+ type GradientType,
21
+ } from "../control-types";
22
+ import { cn } from "../../../lib/utils";
23
+ import {
24
+ addGradientStop,
25
+ formatStopPosition,
26
+ getGradientAngle,
27
+ getGradientBackground,
28
+ getGradientType,
29
+ getIndexedStops,
30
+ getPositionFromTrack,
31
+ getStopCssColor,
32
+ isButtonTarget,
33
+ maxGradientStops,
34
+ minGradientStops,
35
+ parseStopPosition,
36
+ removeGradientStop,
37
+ updateStopAt,
38
+ type IndexedGradientStop,
39
+ } from "./gradient-control-utils";
40
+ import { GradientStopsList } from "./gradient-stop-list";
41
+ import { GradientToolbar } from "./gradient-toolbar";
42
+
43
+ const playControlDragEndSound = () => undefined;
44
+ const playControlDragStartSound = () => undefined;
45
+ const playGradientAngleSound = (_previousAngle: number, _nextAngle: number) =>
46
+ undefined;
47
+ const playGradientStopUpdateSound = (
48
+ _previousStop: GradientStop | undefined,
49
+ _nextStop: Partial<GradientStop>,
50
+ ) => undefined;
51
+
52
+ function useGradientStopSelectionSound(
53
+ _selectedIndex: number | null,
54
+ setSelectedIndex: Dispatch<SetStateAction<number | null>>,
55
+ ) {
56
+ return (nextIndex: number) => setSelectedIndex(nextIndex);
57
+ }
58
+
59
+ export type GradientControlProps = {
60
+ angle?: number;
61
+ gradientType?: GradientType;
62
+ name?: string;
63
+ onValueChange?: ControlValueChangeHandler<{
64
+ angle: number;
65
+ gradientType: GradientType;
66
+ stops: readonly GradientStop[];
67
+ }>;
68
+ stops: readonly GradientStop[];
69
+ };
70
+
71
+ type GradientStopsControllerOptions = {
72
+ angle: number;
73
+ gradientType: GradientType;
74
+ name: string;
75
+ onValueChange?: ControlValueChangeHandler<{
76
+ angle: number;
77
+ gradientType: GradientType;
78
+ stops: readonly GradientStop[];
79
+ }>;
80
+ stops: readonly GradientStop[];
81
+ trackRef: RefObject<HTMLDivElement | null>;
82
+ };
83
+
84
+ type GradientStopActionsOptions = GradientStopsControllerOptions & {
85
+ activeStop: GradientStop | null;
86
+ setSelectedIndex: Dispatch<SetStateAction<number | null>>;
87
+ };
88
+
89
+ function useGradientStopActions({
90
+ activeStop,
91
+ angle,
92
+ gradientType,
93
+ onValueChange,
94
+ setSelectedIndex,
95
+ stops,
96
+ }: GradientStopActionsOptions) {
97
+ function updateGradient(
98
+ nextGradient: {
99
+ angle?: number;
100
+ gradientType?: GradientType;
101
+ stops?: readonly GradientStop[];
102
+ },
103
+ meta?: ControlChangeMeta,
104
+ ): void {
105
+ const nextValue = {
106
+ angle: nextGradient.angle ?? angle,
107
+ gradientType: nextGradient.gradientType ?? gradientType,
108
+ stops: nextGradient.stops ?? stops,
109
+ };
110
+
111
+ if (meta) {
112
+ onValueChange?.(nextValue, meta);
113
+ return;
114
+ }
115
+
116
+ onValueChange?.(nextValue);
117
+ }
118
+
119
+ function updateStop(
120
+ index: number,
121
+ nextStop: Partial<GradientStop>,
122
+ meta?: ControlChangeMeta,
123
+ ): void {
124
+ playGradientStopUpdateSound(stops[index], nextStop);
125
+ updateGradient({ stops: updateStopAt(stops, index, nextStop) }, meta);
126
+ }
127
+
128
+ function addStop(position = "50%"): void {
129
+ if (stops.length >= maxGradientStops) {
130
+ return;
131
+ }
132
+
133
+ const { nextStop, nextStops } = addGradientStop(
134
+ stops,
135
+ activeStop,
136
+ position,
137
+ );
138
+
139
+ updateGradient({ stops: nextStops });
140
+ setSelectedIndex(nextStops.indexOf(nextStop));
141
+ }
142
+
143
+ function removeStop(index: number): void {
144
+ if (stops.length <= minGradientStops) {
145
+ return;
146
+ }
147
+
148
+ const nextStops = removeGradientStop(stops, index);
149
+
150
+ updateGradient({ stops: nextStops });
151
+ setSelectedIndex(
152
+ nextStops.length > 0 ? Math.min(index, nextStops.length - 1) : null,
153
+ );
154
+ }
155
+
156
+ return {
157
+ addStop,
158
+ removeStop,
159
+ updateAngle: (nextAngle: number, meta?: ControlChangeMeta) => {
160
+ playGradientAngleSound(angle, nextAngle);
161
+ updateGradient({ angle: nextAngle }, meta);
162
+ },
163
+ updateGradientType: (nextType: GradientType) =>
164
+ updateGradient({ gradientType: nextType }),
165
+ updateStop,
166
+ };
167
+ }
168
+
169
+ type GradientStopActions = ReturnType<typeof useGradientStopActions>;
170
+
171
+ function getGradientDragHistoryMeta(
172
+ name: string,
173
+ dragHistoryGroupRef: MutableRefObject<string | null>,
174
+ ): ControlChangeMeta {
175
+ dragHistoryGroupRef.current ??= createControlHistoryGroupId(`gradient:${name}`);
176
+
177
+ return {
178
+ history: "merge",
179
+ historyGroup: dragHistoryGroupRef.current,
180
+ };
181
+ }
182
+
183
+ function useGradientStopDragWindowEvents({
184
+ actions,
185
+ dragHistoryGroupRef,
186
+ draggingIndex,
187
+ name,
188
+ setDraggingIndex,
189
+ trackRef,
190
+ }: {
191
+ actions: GradientStopActions;
192
+ dragHistoryGroupRef: MutableRefObject<string | null>;
193
+ draggingIndex: number | null;
194
+ name: string;
195
+ setDraggingIndex: Dispatch<SetStateAction<number | null>>;
196
+ trackRef: RefObject<HTMLDivElement | null>;
197
+ }): void {
198
+ useEffect(() => {
199
+ const activeDraggingIndex = draggingIndex;
200
+
201
+ if (activeDraggingIndex === null) {
202
+ return;
203
+ }
204
+
205
+ const stopIndex = activeDraggingIndex;
206
+
207
+ function handlePointerMove(event: PointerEvent): void {
208
+ actions.updateStop(
209
+ stopIndex,
210
+ {
211
+ position: getPositionFromTrack(trackRef.current, event.clientX),
212
+ },
213
+ getGradientDragHistoryMeta(name, dragHistoryGroupRef),
214
+ );
215
+ }
216
+
217
+ function stopDragging(): void {
218
+ playControlDragEndSound();
219
+ setDraggingIndex(null);
220
+ dragHistoryGroupRef.current = null;
221
+ }
222
+
223
+ window.addEventListener("pointermove", handlePointerMove);
224
+ window.addEventListener("pointerup", stopDragging);
225
+ window.addEventListener("pointercancel", stopDragging);
226
+
227
+ return () => {
228
+ window.removeEventListener("pointermove", handlePointerMove);
229
+ window.removeEventListener("pointerup", stopDragging);
230
+ window.removeEventListener("pointercancel", stopDragging);
231
+ };
232
+ });
233
+ }
234
+
235
+ function useGradientStopsController(options: GradientStopsControllerOptions) {
236
+ const { stops, trackRef } = options;
237
+ const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
238
+ const dragHistoryGroupRef = useRef<string | null>(null);
239
+ const [selectedIndex, setSelectedIndex] = useState<number | null>(0);
240
+ const activeStop =
241
+ selectedIndex === null ? null : (stops[selectedIndex] ?? null);
242
+ const actions = useGradientStopActions({
243
+ ...options,
244
+ activeStop,
245
+ setSelectedIndex,
246
+ });
247
+ const selectStop = useGradientStopSelectionSound(
248
+ selectedIndex,
249
+ setSelectedIndex,
250
+ );
251
+
252
+ useGradientStopDragWindowEvents({
253
+ actions,
254
+ dragHistoryGroupRef,
255
+ draggingIndex,
256
+ name: options.name,
257
+ setDraggingIndex,
258
+ trackRef,
259
+ });
260
+
261
+ function handleTrackPointerDown(
262
+ event: React.PointerEvent<HTMLDivElement>,
263
+ ): void {
264
+ if (
265
+ draggingIndex !== null ||
266
+ stops.length >= maxGradientStops ||
267
+ isButtonTarget(event.target)
268
+ ) {
269
+ return;
270
+ }
271
+
272
+ actions.addStop(getPositionFromTrack(trackRef.current, event.clientX));
273
+ }
274
+
275
+ function handleTrackPointerMove(
276
+ event: React.PointerEvent<HTMLDivElement>,
277
+ ): void {
278
+ if (draggingIndex === null) {
279
+ return;
280
+ }
281
+
282
+ actions.updateStop(
283
+ draggingIndex,
284
+ {
285
+ position: getPositionFromTrack(trackRef.current, event.clientX),
286
+ },
287
+ getGradientDragHistoryMeta(options.name, dragHistoryGroupRef),
288
+ );
289
+ }
290
+
291
+ function handleStartDrag(
292
+ index: number,
293
+ event: React.PointerEvent<HTMLButtonElement>,
294
+ ): void {
295
+ event.preventDefault();
296
+ event.stopPropagation();
297
+ event.currentTarget.focus();
298
+ event.currentTarget.setPointerCapture(event.pointerId);
299
+ playControlDragStartSound();
300
+ dragHistoryGroupRef.current = createControlHistoryGroupId(`gradient:${options.name}`);
301
+ setDraggingIndex(index);
302
+ selectStop(index);
303
+ }
304
+
305
+ function handleStopDoubleClick(
306
+ index: number,
307
+ event: React.MouseEvent<HTMLButtonElement>,
308
+ ): void {
309
+ event.stopPropagation();
310
+ actions.removeStop(index);
311
+ }
312
+
313
+ function handleStopKeyDown(
314
+ index: number,
315
+ event: React.KeyboardEvent<HTMLButtonElement>,
316
+ ): void {
317
+ if (event.key !== "Delete" && event.key !== "Backspace") {
318
+ return;
319
+ }
320
+
321
+ if (selectedIndex !== index) {
322
+ return;
323
+ }
324
+
325
+ event.preventDefault();
326
+ event.stopPropagation();
327
+ actions.removeStop(index);
328
+ }
329
+
330
+ return {
331
+ ...actions,
332
+ draggingIndex,
333
+ handleStopDoubleClick,
334
+ handleStopKeyDown,
335
+ handleStartDrag,
336
+ handleTrackPointerDown,
337
+ handleTrackPointerMove,
338
+ indexedStops: getIndexedStops(stops),
339
+ selectStop,
340
+ selectedIndex,
341
+ setDraggingIndex: (nextIndex: number | null) => {
342
+ setDraggingIndex(nextIndex);
343
+ if (nextIndex === null) {
344
+ dragHistoryGroupRef.current = null;
345
+ }
346
+ },
347
+ setSelectedIndex,
348
+ };
349
+ }
350
+
351
+ const gradientStopPinEdgeInset = 2;
352
+
353
+ function getGradientStopPinLeftPosition(position: number): string {
354
+ const stopPosition = formatStopPosition(position);
355
+ const pixelOffset = gradientStopPinEdgeInset * (1 - position * 2);
356
+
357
+ if (Math.abs(pixelOffset) < 0.01) {
358
+ return stopPosition;
359
+ }
360
+
361
+ const offsetOperator = pixelOffset > 0 ? "+" : "-";
362
+ const offsetValue = Number(Math.abs(pixelOffset).toFixed(2));
363
+
364
+ return `calc(${stopPosition} ${offsetOperator} ${offsetValue}px)`;
365
+ }
366
+
367
+ function GradientStopPin({
368
+ isDragging,
369
+ isSelected,
370
+ onDoubleClick,
371
+ onKeyDown,
372
+ onPointerDown,
373
+ stop,
374
+ }: {
375
+ isDragging: boolean;
376
+ isSelected: boolean;
377
+ onDoubleClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
378
+ onKeyDown: (event: React.KeyboardEvent<HTMLButtonElement>) => void;
379
+ onPointerDown: (event: React.PointerEvent<HTMLButtonElement>) => void;
380
+ stop: IndexedGradientStop;
381
+ }): React.JSX.Element {
382
+ const stopPosition = parseStopPosition(stop.position);
383
+
384
+ return (
385
+ <button
386
+ aria-label={`Gradient stop ${stop.originalIndex + 1}`}
387
+ aria-pressed={isSelected}
388
+ className={cn(
389
+ "absolute top-1 z-10 flex touch-none -translate-x-1/2 cursor-grab flex-col items-center rounded-lg outline-none",
390
+ "active:cursor-grabbing",
391
+ isDragging && "cursor-grabbing",
392
+ )}
393
+ onDoubleClick={onDoubleClick}
394
+ onKeyDown={onKeyDown}
395
+ onPointerDown={onPointerDown}
396
+ style={{ left: getGradientStopPinLeftPosition(stopPosition) }}
397
+ type="button"
398
+ >
399
+ <span
400
+ className={cn(
401
+ "flex size-[22px] items-center justify-center rounded-md bg-[color:var(--muted)] shadow-[0_4px_7px_color-mix(in_oklab,var(--background)_30%,transparent)] transition-colors",
402
+ isSelected && "bg-[color:var(--accent)]",
403
+ )}
404
+ >
405
+ <span
406
+ aria-hidden="true"
407
+ className={cn(
408
+ "h-3.5 max-h-3.5 min-h-3.5 w-3.5 max-w-3.5 min-w-3.5 flex-none rounded-[4px] border border-[color:color-mix(in_oklab,var(--border)_10%,transparent)]",
409
+ isSelected && selectedItemBorderClassName,
410
+ )}
411
+ style={{ backgroundColor: getStopCssColor(stop) }}
412
+ />
413
+ </span>
414
+ <svg
415
+ aria-hidden="true"
416
+ className={cn(
417
+ "h-1 w-2.5 text-[color:var(--muted)] transition-colors",
418
+ isSelected && "text-[color:var(--accent)]",
419
+ )}
420
+ fill="none"
421
+ viewBox="0 0 10 4"
422
+ >
423
+ <path d="M0 0H10L5.72 3.42Q5 4.08 4.28 3.42L0 0Z" fill="currentColor" />
424
+ </svg>
425
+ </button>
426
+ );
427
+ }
428
+
429
+ function GradientStopsTrack({
430
+ gradient,
431
+ onDragEnd,
432
+ onPointerDown,
433
+ onPointerMove,
434
+ onRemoveStop,
435
+ onRemoveStopByKey,
436
+ onStartDrag,
437
+ selectedIndex,
438
+ stops,
439
+ trackRef,
440
+ draggingIndex,
441
+ }: {
442
+ gradient: string;
443
+ draggingIndex: number | null;
444
+ onDragEnd: () => void;
445
+ onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => void;
446
+ onPointerMove: (event: React.PointerEvent<HTMLDivElement>) => void;
447
+ onRemoveStop: (
448
+ index: number,
449
+ event: React.MouseEvent<HTMLButtonElement>,
450
+ ) => void;
451
+ onRemoveStopByKey: (
452
+ index: number,
453
+ event: React.KeyboardEvent<HTMLButtonElement>,
454
+ ) => void;
455
+ onStartDrag: (
456
+ index: number,
457
+ event: React.PointerEvent<HTMLButtonElement>,
458
+ ) => void;
459
+ selectedIndex: number | null;
460
+ stops: readonly IndexedGradientStop[];
461
+ trackRef: React.RefObject<HTMLDivElement | null>;
462
+ }): React.JSX.Element {
463
+ return (
464
+ <div
465
+ aria-label="Gradient stops track"
466
+ className="app-no-drag relative mt-1 h-12 w-full touch-none cursor-crosshair"
467
+ onPointerCancel={onDragEnd}
468
+ onPointerDown={onPointerDown}
469
+ onPointerMove={onPointerMove}
470
+ onPointerUp={onDragEnd}
471
+ ref={trackRef}
472
+ >
473
+ <div className="absolute inset-x-0 top-4 h-6 overflow-hidden rounded-md border border-[color:color-mix(in_oklab,var(--border)_10%,transparent)]">
474
+ <div
475
+ aria-hidden="true"
476
+ className="absolute inset-0 rounded-[inherit]"
477
+ style={{ background: gradient }}
478
+ />
479
+ </div>
480
+ {stops.map((stop) => (
481
+ <GradientStopPin
482
+ isDragging={draggingIndex === stop.originalIndex}
483
+ isSelected={selectedIndex === stop.originalIndex}
484
+ key={stop.originalIndex}
485
+ onDoubleClick={(event) => onRemoveStop(stop.originalIndex, event)}
486
+ onKeyDown={(event) => onRemoveStopByKey(stop.originalIndex, event)}
487
+ onPointerDown={(event) => onStartDrag(stop.originalIndex, event)}
488
+ stop={stop}
489
+ />
490
+ ))}
491
+ </div>
492
+ );
493
+ }
494
+
495
+ export function GradientControl({
496
+ angle: angleProp,
497
+ gradientType: gradientTypeProp,
498
+ name = "Gradient",
499
+ onValueChange,
500
+ stops,
501
+ }: GradientControlProps): React.JSX.Element {
502
+ const trackRef = useRef<HTMLDivElement | null>(null);
503
+ const [uncontrolledAngle, setUncontrolledAngle] = useState(() =>
504
+ getGradientAngle(angleProp),
505
+ );
506
+ const [uncontrolledGradientType, setUncontrolledGradientType] = useState(() =>
507
+ getGradientType(gradientTypeProp),
508
+ );
509
+ const angle =
510
+ typeof angleProp === "undefined"
511
+ ? uncontrolledAngle
512
+ : getGradientAngle(angleProp);
513
+ const gradientType =
514
+ typeof gradientTypeProp === "undefined"
515
+ ? uncontrolledGradientType
516
+ : getGradientType(gradientTypeProp);
517
+
518
+ useEffect(() => {
519
+ if (typeof angleProp !== "undefined") {
520
+ setUncontrolledAngle(getGradientAngle(angleProp));
521
+ }
522
+ }, [angleProp]);
523
+
524
+ useEffect(() => {
525
+ if (typeof gradientTypeProp !== "undefined") {
526
+ setUncontrolledGradientType(getGradientType(gradientTypeProp));
527
+ }
528
+ }, [gradientTypeProp]);
529
+
530
+ function handleValueChange(
531
+ value: {
532
+ angle: number;
533
+ gradientType: GradientType;
534
+ stops: readonly GradientStop[];
535
+ },
536
+ meta?: ControlChangeMeta,
537
+ ): void {
538
+ if (typeof angleProp === "undefined") {
539
+ setUncontrolledAngle(value.angle);
540
+ }
541
+
542
+ if (typeof gradientTypeProp === "undefined") {
543
+ setUncontrolledGradientType(value.gradientType);
544
+ }
545
+
546
+ if (meta) {
547
+ onValueChange?.(value, meta);
548
+ return;
549
+ }
550
+
551
+ onValueChange?.(value);
552
+ }
553
+
554
+ const controller = useGradientStopsController({
555
+ angle,
556
+ gradientType,
557
+ name,
558
+ onValueChange: handleValueChange,
559
+ stops,
560
+ trackRef,
561
+ });
562
+
563
+ return (
564
+ <Field className="min-w-0 !gap-[3px]">
565
+ <div
566
+ className="flex min-w-0 flex-col gap-3"
567
+ data-slot="gradient-stops-control-main"
568
+ >
569
+ <div className="flex h-fit min-w-0 items-center justify-start">
570
+ <GradientToolbar
571
+ angle={angle}
572
+ name={name}
573
+ onAngleChange={controller.updateAngle}
574
+ onTypeChange={controller.updateGradientType}
575
+ type={gradientType}
576
+ />
577
+ </div>
578
+ <div className="min-w-0">
579
+ <GradientStopsTrack
580
+ draggingIndex={controller.draggingIndex}
581
+ gradient={getGradientBackground(gradientType, stops, angle)}
582
+ onDragEnd={() => controller.setDraggingIndex(null)}
583
+ onPointerDown={controller.handleTrackPointerDown}
584
+ onPointerMove={controller.handleTrackPointerMove}
585
+ onRemoveStop={controller.handleStopDoubleClick}
586
+ onRemoveStopByKey={controller.handleStopKeyDown}
587
+ onStartDrag={controller.handleStartDrag}
588
+ selectedIndex={controller.selectedIndex}
589
+ stops={controller.indexedStops}
590
+ trackRef={trackRef}
591
+ />
592
+ </div>
593
+ </div>
594
+ <GradientStopsList
595
+ onAdd={controller.addStop}
596
+ onColorChange={(index, nextColor) =>
597
+ controller.updateStop(index, { color: nextColor })
598
+ }
599
+ onOpacityChange={(index, nextOpacity) =>
600
+ controller.updateStop(index, { opacity: nextOpacity })
601
+ }
602
+ onPositionChange={(index, nextPosition) =>
603
+ controller.updateStop(index, { position: nextPosition })
604
+ }
605
+ onRemove={controller.removeStop}
606
+ onSelect={controller.selectStop}
607
+ selectedIndex={controller.selectedIndex}
608
+ stops={controller.indexedStops}
609
+ />
610
+ </Field>
611
+ );
612
+ }