@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,401 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { ControlFieldLabel } from "../../control-layout";
6
+ import { EditableSliderValueLabel, Field, Input } from "../../primitives";
7
+ import {
8
+ createControlHistoryGroupId,
9
+ type ControlChangeMeta,
10
+ type ControlValueChangeHandler,
11
+ type CurvePoint,
12
+ } from "../control-types";
13
+ import { cn } from "../../../lib/utils";
14
+
15
+ export type VectorControlValue = {
16
+ x: string;
17
+ y: string;
18
+ };
19
+
20
+ export type VectorPadVariant =
21
+ | "default"
22
+ | "whiteBalance"
23
+ | "colorBalance"
24
+ | "chromaOffset"
25
+ | "toneBias";
26
+
27
+ export type VectorControlProps = VectorControlValue & {
28
+ defaultValue?: Partial<VectorControlValue>;
29
+ name: string;
30
+ onValueChange?: ControlValueChangeHandler<VectorControlValue>;
31
+ padShape?: "compact" | "square";
32
+ padVariant?: VectorPadVariant;
33
+ xLabel?: string;
34
+ yLabel?: string;
35
+ };
36
+
37
+ function clamp(value: number, min = 0, max = 1): number {
38
+ return Math.min(max, Math.max(min, value));
39
+ }
40
+
41
+ function pointFromEvent(event: React.PointerEvent<HTMLElement>): CurvePoint {
42
+ const rect = event.currentTarget.getBoundingClientRect();
43
+ const x = clamp((event.clientX - rect.left) / rect.width);
44
+ const y = clamp(1 - (event.clientY - rect.top) / rect.height);
45
+
46
+ return { x, y };
47
+ }
48
+
49
+ type VectorPadStyle = React.CSSProperties & {
50
+ "--xy-pad-display-x": string;
51
+ "--xy-pad-display-y": string;
52
+ "--xy-pad-handle-margin": string;
53
+ "--xy-pad-x": string;
54
+ "--xy-pad-y": string;
55
+ };
56
+
57
+ const whiteBalancePadBackgroundImage =
58
+ "linear-gradient(90deg,color-mix(in oklab, #2f7cff 72%, transparent),color-mix(in oklab, var(--foreground) 8%, transparent) 50%,color-mix(in oklab, #ff9a22 78%, transparent)),linear-gradient(180deg,color-mix(in oklab, #d84f9a 68%, transparent),transparent 49%,color-mix(in oklab, #42ba62 72%, transparent)),radial-gradient(circle at 50% 50%,color-mix(in oklab, var(--foreground) 7%, transparent),color-mix(in oklab, var(--background) 14%, transparent) 62%)";
59
+
60
+ const colorBalancePadBackgroundImage =
61
+ "linear-gradient(90deg,color-mix(in oklab, #1dbdd3 70%, transparent),color-mix(in oklab, var(--foreground) 7%, transparent) 50%,color-mix(in oklab, #ff4a45 74%, transparent)),linear-gradient(180deg,color-mix(in oklab, #f2d94c 66%, transparent),transparent 50%,color-mix(in oklab, #315cff 70%, transparent)),radial-gradient(circle at 50% 50%,color-mix(in oklab, var(--foreground) 7%, transparent),color-mix(in oklab, var(--background) 14%, transparent) 62%)";
62
+
63
+ const chromaOffsetPadBackgroundImage =
64
+ "linear-gradient(90deg,color-mix(in oklab, #ff2f55 68%, transparent),transparent 38%,transparent 62%,color-mix(in oklab, #25c7ff 70%, transparent)),linear-gradient(180deg,color-mix(in oklab, #56ff72 54%, transparent),transparent 42%,transparent 58%,color-mix(in oklab, #7b5cff 62%, transparent)),radial-gradient(circle at 50% 50%,color-mix(in oklab, var(--foreground) 8%, transparent),color-mix(in oklab, var(--background) 14%, transparent) 62%)";
65
+
66
+ const toneBiasPadBackgroundImage =
67
+ "linear-gradient(90deg,color-mix(in oklab, #3f56ff 60%, transparent),color-mix(in oklab, var(--foreground) 7%, transparent) 50%,color-mix(in oklab, #ffb23f 68%, transparent)),linear-gradient(180deg,color-mix(in oklab, #f05bb5 55%, transparent),transparent 50%,color-mix(in oklab, #1fbf9a 60%, transparent)),radial-gradient(circle at 50% 50%,color-mix(in oklab, var(--foreground) 7%, transparent),color-mix(in oklab, var(--background) 14%, transparent) 62%)";
68
+
69
+ const vectorPadBackgroundImages: Partial<Record<VectorPadVariant, string>> = {
70
+ chromaOffset: chromaOffsetPadBackgroundImage,
71
+ colorBalance: colorBalancePadBackgroundImage,
72
+ toneBias: toneBiasPadBackgroundImage,
73
+ whiteBalance: whiteBalancePadBackgroundImage,
74
+ };
75
+
76
+ function getVectorPoint(x: string, y: string): VectorPadStyle {
77
+ const parsedX = Number.parseFloat(x);
78
+ const parsedY = Number.parseFloat(y);
79
+ const clampedX = Number.isFinite(parsedX) ? clamp(parsedX, -1, 1) : 0;
80
+ const clampedY = Number.isFinite(parsedY) ? clamp(parsedY, -1, 1) : 0;
81
+ const xPosition = `${(clampedX + 1) * 50}%`;
82
+ const yPosition = `${(1 - (clampedY + 1) / 2) * 100}%`;
83
+
84
+ return {
85
+ "--xy-pad-display-x":
86
+ "clamp(var(--xy-pad-handle-margin), var(--xy-pad-x), calc(100% - var(--xy-pad-handle-margin)))",
87
+ "--xy-pad-display-y":
88
+ "clamp(var(--xy-pad-handle-margin), var(--xy-pad-y), calc(100% - var(--xy-pad-handle-margin)))",
89
+ "--xy-pad-handle-margin": "12px",
90
+ "--xy-pad-x": xPosition,
91
+ "--xy-pad-y": yPosition,
92
+ };
93
+ }
94
+
95
+ function getVectorValueLabel(x: string, y: string): string {
96
+ return `${x}, ${y}`;
97
+ }
98
+
99
+ function normalizeVectorCoordinate(value: string | undefined): string {
100
+ return typeof value === "string" && value.trim() ? value : "0.00";
101
+ }
102
+
103
+ function parseVectorValueDraft(value: string): VectorControlValue | null {
104
+ const matches = value.match(/[+-]?(?:\d+(?:[.,]\d+)?|[.,]\d+)/g);
105
+
106
+ if (!matches || matches.length < 2) {
107
+ return null;
108
+ }
109
+
110
+ const [rawX, rawY] = matches;
111
+ const nextX = Number.parseFloat(rawX.replace(",", "."));
112
+ const nextY = Number.parseFloat(rawY.replace(",", "."));
113
+
114
+ if (!Number.isFinite(nextX) || !Number.isFinite(nextY)) {
115
+ return null;
116
+ }
117
+
118
+ return {
119
+ x: clamp(nextX, -1, 1).toFixed(2),
120
+ y: clamp(nextY, -1, 1).toFixed(2),
121
+ };
122
+ }
123
+
124
+ function VectorSizeField({
125
+ defaultValue,
126
+ name,
127
+ onValueChange,
128
+ x,
129
+ xLabel = "X",
130
+ y,
131
+ yLabel = "Y",
132
+ }: VectorControlProps): React.JSX.Element {
133
+ const [draftValue, setDraftValue] = React.useState({ x, y });
134
+ const committedValueRef = React.useRef({ x, y });
135
+ const defaultValueRef = React.useRef({
136
+ x: normalizeVectorCoordinate(defaultValue?.x),
137
+ y: normalizeVectorCoordinate(defaultValue?.y),
138
+ });
139
+
140
+ React.useEffect(() => {
141
+ committedValueRef.current = { x, y };
142
+ setDraftValue({ x, y });
143
+ }, [x, y]);
144
+
145
+ React.useEffect(() => {
146
+ defaultValueRef.current = {
147
+ x: normalizeVectorCoordinate(defaultValue?.x),
148
+ y: normalizeVectorCoordinate(defaultValue?.y),
149
+ };
150
+ }, [defaultValue?.x, defaultValue?.y]);
151
+
152
+ function commitVector(): void {
153
+ const nextValue = {
154
+ x: draftValue.x.trim() === "" ? defaultValueRef.current.x : draftValue.x,
155
+ y: draftValue.y.trim() === "" ? defaultValueRef.current.y : draftValue.y,
156
+ };
157
+
158
+ setDraftValue(nextValue);
159
+
160
+ if (
161
+ nextValue.x !== committedValueRef.current.x ||
162
+ nextValue.y !== committedValueRef.current.y
163
+ ) {
164
+ onValueChange?.(nextValue);
165
+ }
166
+ }
167
+
168
+ function cancelDraft(): void {
169
+ setDraftValue(committedValueRef.current);
170
+ }
171
+
172
+ function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
173
+ if (event.key === "Enter") {
174
+ event.preventDefault();
175
+ commitVector();
176
+ event.currentTarget.blur();
177
+ return;
178
+ }
179
+
180
+ if (event.key === "Escape") {
181
+ event.preventDefault();
182
+ cancelDraft();
183
+ event.currentTarget.blur();
184
+ }
185
+ }
186
+
187
+ return (
188
+ <Field className="min-w-0 gap-2">
189
+ <div className="flex items-center justify-between gap-3">
190
+ <ControlFieldLabel>{name}</ControlFieldLabel>
191
+ </div>
192
+ <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-1.5">
193
+ <Input
194
+ aria-label={`${name} ${xLabel}`}
195
+ className="font-mono"
196
+ onBlur={commitVector}
197
+ onChange={(event) =>
198
+ setDraftValue((current) => ({ ...current, x: event.target.value }))
199
+ }
200
+ onKeyDown={handleKeyDown}
201
+ size="default"
202
+ value={draftValue.x}
203
+ />
204
+ <Input
205
+ aria-label={`${name} ${yLabel}`}
206
+ className="font-mono"
207
+ onBlur={commitVector}
208
+ onChange={(event) =>
209
+ setDraftValue((current) => ({ ...current, y: event.target.value }))
210
+ }
211
+ onKeyDown={handleKeyDown}
212
+ size="default"
213
+ value={draftValue.y}
214
+ />
215
+ </div>
216
+ </Field>
217
+ );
218
+ }
219
+
220
+ function VectorPadGuides({ isDragging }: { isDragging: boolean }): React.JSX.Element {
221
+ const motionClass = isDragging
222
+ ? "transition-none"
223
+ : "transition-[top] duration-[260ms] ease-[cubic-bezier(0.34,1.56,0.64,1)]";
224
+ const verticalMotionClass = isDragging
225
+ ? "transition-none"
226
+ : "transition-[left] duration-[260ms] ease-[cubic-bezier(0.34,1.56,0.64,1)]";
227
+
228
+ return (
229
+ <>
230
+ <span
231
+ aria-hidden="true"
232
+ className={cn(
233
+ "pointer-events-none absolute inset-x-0 z-10 h-px bg-[linear-gradient(90deg,transparent,color-mix(in_oklab,var(--foreground)_20%,transparent),transparent)]",
234
+ motionClass,
235
+ )}
236
+ style={{ top: "var(--xy-pad-display-y)" }}
237
+ />
238
+ <span
239
+ aria-hidden="true"
240
+ className={cn(
241
+ "pointer-events-none absolute top-0 bottom-0 z-10 w-px bg-[linear-gradient(180deg,transparent,color-mix(in_oklab,var(--foreground)_20%,transparent),transparent)]",
242
+ verticalMotionClass,
243
+ )}
244
+ style={{ left: "var(--xy-pad-display-x)" }}
245
+ />
246
+ </>
247
+ );
248
+ }
249
+
250
+ function VectorPadHandle({ isDragging }: { isDragging: boolean }): React.JSX.Element {
251
+ return (
252
+ <div
253
+ aria-hidden="true"
254
+ className={cn(
255
+ "xy-handle group/xy-handle pointer-events-auto absolute z-20 size-3 -translate-x-1/2 -translate-y-1/2 cursor-default will-change-[left,top,transform]",
256
+ isDragging
257
+ ? "cursor-pointer transition-transform duration-[120ms] ease-[cubic-bezier(0.22,1,0.36,1)]"
258
+ : "transition-[left,top,transform] duration-[260ms,260ms,120ms] ease-[cubic-bezier(0.34,1.56,0.64,1),cubic-bezier(0.34,1.56,0.64,1),cubic-bezier(0.22,1,0.36,1)] hover:cursor-pointer",
259
+ )}
260
+ style={{ left: "var(--xy-pad-display-x)", top: "var(--xy-pad-display-y)" }}
261
+ >
262
+ <span
263
+ aria-hidden="true"
264
+ className={cn(
265
+ "pointer-events-none absolute inset-[-5px] rounded-full bg-[color:color-mix(in_oklab,var(--foreground)_12%,transparent)] opacity-0 blur-[8px] transition-[opacity,scale] duration-200 ease-out",
266
+ isDragging ? "scale-[1.45] opacity-100" : "scale-95",
267
+ )}
268
+ />
269
+ <span
270
+ className={cn(
271
+ "relative block size-full rounded-full bg-[radial-gradient(circle_at_30%_30%,color-mix(in_oklab,var(--foreground)_95%,transparent),color-mix(in_oklab,var(--foreground)_76%,transparent)),linear-gradient(180deg,color-mix(in_oklab,var(--foreground)_20%,transparent),color-mix(in_oklab,var(--foreground)_6%,transparent))] shadow-[0_4px_10px_color-mix(in_oklab,var(--background)_20%,transparent)] transition-[scale,background-color,box-shadow] duration-200 ease-out will-change-transform motion-reduce:transition-none",
272
+ isDragging ? "scale-[1.3333]" : "group-hover/xy-handle:scale-[1.3333]",
273
+ )}
274
+ />
275
+ </div>
276
+ );
277
+ }
278
+
279
+ function VectorPadField({
280
+ name,
281
+ onValueChange,
282
+ padShape = "compact",
283
+ padVariant = "default",
284
+ x,
285
+ y,
286
+ }: VectorControlProps): React.JSX.Element {
287
+ const [isPointerDragging, setIsPointerDragging] = React.useState(false);
288
+ const normalizedX = normalizeVectorCoordinate(x);
289
+ const normalizedY = normalizeVectorCoordinate(y);
290
+ const point = getVectorPoint(normalizedX, normalizedY);
291
+ const valueLabel = getVectorValueLabel(normalizedX, normalizedY);
292
+ const vectorPadBackgroundImage = vectorPadBackgroundImages[padVariant];
293
+ const liveHistoryGroupRef = React.useRef<string | null>(null);
294
+ const updateVector = (
295
+ nextX: string,
296
+ nextY: string,
297
+ meta?: ControlChangeMeta,
298
+ ) => {
299
+ if (meta) {
300
+ onValueChange?.({ x: nextX, y: nextY }, meta);
301
+ return;
302
+ }
303
+
304
+ onValueChange?.({ x: nextX, y: nextY });
305
+ };
306
+ const commitVectorValue = (nextValue: string) => {
307
+ const nextVector = parseVectorValueDraft(nextValue);
308
+
309
+ if (nextVector) {
310
+ updateVector(nextVector.x, nextVector.y);
311
+ }
312
+ };
313
+
314
+ function getLiveHistoryMeta(): ControlChangeMeta {
315
+ liveHistoryGroupRef.current ??= createControlHistoryGroupId(`vector:${name}`);
316
+
317
+ return {
318
+ history: "merge",
319
+ historyGroup: liveHistoryGroupRef.current,
320
+ };
321
+ }
322
+
323
+ function updateFromPointer(event: React.PointerEvent<HTMLButtonElement>): void {
324
+ const nextPoint = pointFromEvent(event);
325
+
326
+ updateVector(
327
+ (nextPoint.x * 2 - 1).toFixed(2),
328
+ (nextPoint.y * 2 - 1).toFixed(2),
329
+ getLiveHistoryMeta(),
330
+ );
331
+ }
332
+
333
+ function stopPointerDrag(): void {
334
+ setIsPointerDragging(false);
335
+ liveHistoryGroupRef.current = null;
336
+ }
337
+
338
+ return (
339
+ <Field className="min-w-0 gap-2">
340
+ <div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3">
341
+ <ControlFieldLabel className="min-w-0">{name}</ControlFieldLabel>
342
+ <EditableSliderValueLabel
343
+ ariaLabel={`${name} value`}
344
+ maxValueLabel="-1.00, -1.00"
345
+ onCommit={commitVectorValue}
346
+ valueLabel={valueLabel}
347
+ />
348
+ </div>
349
+ <button
350
+ aria-label={`${name} X/Y pad`}
351
+ className={cn(
352
+ "relative w-full cursor-default! touch-none overflow-hidden rounded-[calc(var(--radius)+2px)] bg-[linear-gradient(180deg,color-mix(in_oklab,var(--foreground)_4%,transparent),color-mix(in_oklab,var(--foreground)_1%,transparent))] focus-visible:shadow-[0_0_0_3px_color-mix(in_oklab,var(--foreground)_12%,transparent)] focus-visible:outline-none",
353
+ padShape === "square" ? "aspect-square" : "h-[142px]",
354
+ )}
355
+ data-vector-pad-shape={padShape}
356
+ data-vector-pad-variant={padVariant}
357
+ onLostPointerCapture={stopPointerDrag}
358
+ onPointerCancel={stopPointerDrag}
359
+ onPointerDown={(event) => {
360
+ setIsPointerDragging(true);
361
+ event.currentTarget.setPointerCapture(event.pointerId);
362
+ updateFromPointer(event);
363
+ }}
364
+ onPointerMove={(event) => {
365
+ if (event.buttons === 1) {
366
+ updateFromPointer(event);
367
+ }
368
+ }}
369
+ onPointerUp={stopPointerDrag}
370
+ style={point}
371
+ type="button"
372
+ >
373
+ {vectorPadBackgroundImage ? (
374
+ <span
375
+ aria-hidden="true"
376
+ className="pointer-events-none absolute inset-0 rounded-[inherit] opacity-40"
377
+ style={{ backgroundImage: vectorPadBackgroundImage }}
378
+ />
379
+ ) : null}
380
+ <span
381
+ aria-hidden="true"
382
+ className="pointer-events-none absolute inset-0 rounded-[inherit] bg-[radial-gradient(circle,color-mix(in_oklab,var(--foreground)_8%,transparent)_1px,transparent_1px)] bg-[length:14px_14px]"
383
+ />
384
+ <VectorPadGuides isDragging={isPointerDragging} />
385
+ <VectorPadHandle isDragging={isPointerDragging} />
386
+ <span
387
+ aria-hidden="true"
388
+ className="pointer-events-none absolute inset-0 z-30 rounded-[inherit] border border-[color:color-mix(in_oklab,var(--border)_10%,transparent)]"
389
+ />
390
+ </button>
391
+ </Field>
392
+ );
393
+ }
394
+
395
+ export function VectorControl(props: VectorControlProps): React.JSX.Element {
396
+ if (props.xLabel === "Width" || props.yLabel === "Height") {
397
+ return <VectorSizeField {...props} />;
398
+ }
399
+
400
+ return <VectorPadField {...props} />;
401
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ export { Panel } from "./panel";
4
+ export type { PanelProps } from "./panel";
5
+ export { PanelActions } from "./panel-actions";
6
+ export type {
7
+ PanelActionIconName,
8
+ PanelActionObjectOption,
9
+ PanelActionOption,
10
+ PanelActionsProps,
11
+ } from "./panel-actions";
12
+ export { PanelHeader } from "./panel-header";
13
+ export {
14
+ PanelIconButton,
15
+ stopPanelHeaderButtonPointerDown,
16
+ } from "./panel-icon-button";
17
+ export { PanelSection } from "./panel-section";
18
+ export type { PanelSectionProps } from "./panel-section";
19
+ export { PanelContentSurface, PanelSurface } from "./panel-surface";
@@ -0,0 +1,165 @@
1
+ "use client";
2
+
3
+ import type { ReactElement, ReactNode } from "react";
4
+ import * as React from "react";
5
+ import {
6
+ ArrowCounterClockwiseIcon,
7
+ CheckIcon,
8
+ CopySimpleIcon,
9
+ DownloadSimpleIcon,
10
+ EraserIcon,
11
+ ExportIcon,
12
+ MagicWandIcon,
13
+ ShuffleIcon,
14
+ UploadSimpleIcon,
15
+ } from "@phosphor-icons/react";
16
+
17
+ import { Button } from "../primitives";
18
+ import { cn } from "../../lib/utils";
19
+
20
+ export type PanelActionIconName =
21
+ | "check"
22
+ | "copy"
23
+ | "download"
24
+ | "download-simple"
25
+ | "eraser"
26
+ | "export"
27
+ | "rotate-ccw"
28
+ | "shuffle"
29
+ | "upload-simple"
30
+ | "wand-sparkles";
31
+
32
+ const panelActionIconComponents = {
33
+ check: CheckIcon,
34
+ copy: CopySimpleIcon,
35
+ download: DownloadSimpleIcon,
36
+ "download-simple": DownloadSimpleIcon,
37
+ eraser: EraserIcon,
38
+ export: ExportIcon,
39
+ "rotate-ccw": ArrowCounterClockwiseIcon,
40
+ shuffle: ShuffleIcon,
41
+ "upload-simple": UploadSimpleIcon,
42
+ "wand-sparkles": MagicWandIcon,
43
+ } as const;
44
+
45
+ type PanelActionVariant = NonNullable<
46
+ React.ComponentProps<typeof Button>["variant"]
47
+ >;
48
+
49
+ export type PanelActionObjectOption = {
50
+ children?: ReactNode;
51
+ className?: string;
52
+ icon?: PanelActionIconName | ReactElement;
53
+ name: string;
54
+ onClick?: React.MouseEventHandler<HTMLButtonElement>;
55
+ value?: string;
56
+ variant: PanelActionVariant;
57
+ };
58
+
59
+ export type PanelActionOption = PanelActionObjectOption;
60
+
61
+ export type PanelActionsProps = {
62
+ actions?: readonly PanelActionOption[];
63
+ children?: ReactNode;
64
+ className?: string;
65
+ columns?: 1 | 2;
66
+ onAction?: (value: string) => void;
67
+ };
68
+
69
+ function getPanelActionValue(action: PanelActionOption): string {
70
+ return action.value ?? action.name;
71
+ }
72
+
73
+ function getPanelActionContent(action: PanelActionOption): ReactNode {
74
+ return action.children ?? action.name;
75
+ }
76
+
77
+ function getPanelActionIcon(action: PanelActionOption): ReactNode {
78
+ if (action.icon == null) {
79
+ return null;
80
+ }
81
+
82
+ if (typeof action.icon !== "string") {
83
+ return action.icon;
84
+ }
85
+
86
+ const Icon = panelActionIconComponents[action.icon];
87
+
88
+ return <Icon data-icon="inline-start" />;
89
+ }
90
+
91
+ function getPanelActionAriaLabel(
92
+ action: PanelActionOption,
93
+ content: ReactNode,
94
+ ): string | undefined {
95
+ if (typeof content === "string") {
96
+ return undefined;
97
+ }
98
+
99
+ return action.name;
100
+ }
101
+
102
+ function shouldSpanFullActionRow(
103
+ index: number,
104
+ actionCount: number,
105
+ columns: 1 | 2,
106
+ ): boolean {
107
+ return columns === 2 && actionCount % 2 === 1 && index === actionCount - 1;
108
+ }
109
+
110
+ export function PanelActions({
111
+ actions,
112
+ children,
113
+ className,
114
+ columns,
115
+ onAction,
116
+ }: PanelActionsProps): React.JSX.Element {
117
+ const actionCount = actions?.length ?? React.Children.count(children);
118
+ const resolvedColumns = columns ?? (actionCount > 1 ? 2 : 1);
119
+
120
+ return (
121
+ <div
122
+ className={cn(
123
+ "grid min-w-0 gap-2",
124
+ resolvedColumns === 2 ? "grid-cols-2" : "grid-cols-1",
125
+ className,
126
+ )}
127
+ data-slot="panel-actions"
128
+ >
129
+ {actions
130
+ ? actions.map((action, index) => {
131
+ const actionContent = getPanelActionContent(action);
132
+ const actionValue = getPanelActionValue(action);
133
+ const handleActionClick: React.MouseEventHandler<
134
+ HTMLButtonElement
135
+ > = (event) => {
136
+ action.onClick?.(event);
137
+
138
+ if (!event.defaultPrevented) {
139
+ onAction?.(actionValue);
140
+ }
141
+ };
142
+
143
+ return (
144
+ <Button
145
+ aria-label={getPanelActionAriaLabel(action, actionContent)}
146
+ className={cn(
147
+ "w-full",
148
+ shouldSpanFullActionRow(index, actionCount, resolvedColumns) &&
149
+ "col-span-2",
150
+ action.className,
151
+ )}
152
+ key={actionValue}
153
+ onClick={handleActionClick}
154
+ type="button"
155
+ variant={action.variant}
156
+ >
157
+ {getPanelActionIcon(action)}
158
+ {actionContent}
159
+ </Button>
160
+ );
161
+ })
162
+ : children}
163
+ </div>
164
+ );
165
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ArrowCounterClockwiseIcon } from "@phosphor-icons/react";
5
+
6
+ import { PrimitiveArrowIcon } from "../primitives";
7
+ import {
8
+ PanelIconButton,
9
+ stopPanelHeaderButtonPointerDown,
10
+ } from "./panel-icon-button";
11
+
12
+ type PanelHeaderProps = {
13
+ collapsed: boolean;
14
+ collapseLabel?: string;
15
+ expandLabel?: string;
16
+ onResetControls?: () => void;
17
+ onToggleCollapsed: () => void;
18
+ title: string;
19
+ };
20
+
21
+ export function PanelHeader({
22
+ collapsed,
23
+ collapseLabel = "Collapse controls",
24
+ expandLabel = "Expand controls",
25
+ onResetControls,
26
+ onToggleCollapsed,
27
+ title,
28
+ }: PanelHeaderProps): React.JSX.Element {
29
+ return (
30
+ <div className="shrink-0" data-slot="properties-panel-header-shell">
31
+ <div
32
+ className="flex h-9 touch-none items-center justify-between gap-3 pr-1 pl-3 hover:cursor-grab active:cursor-grabbing"
33
+ data-panel-drag-handle=""
34
+ data-slot="properties-panel-header"
35
+ >
36
+ <p className="m-0 min-w-0 truncate text-xs-plus font-medium text-[color:var(--foreground)]">
37
+ {title}
38
+ </p>
39
+ <div className="inline-flex shrink-0 items-center gap-1">
40
+ {collapsed || !onResetControls ? null : (
41
+ <PanelIconButton
42
+ label="Reset controls"
43
+ onClick={onResetControls}
44
+ onPointerDown={stopPanelHeaderButtonPointerDown}
45
+ spinOnClick
46
+ >
47
+ <ArrowCounterClockwiseIcon />
48
+ </PanelIconButton>
49
+ )}
50
+ <PanelIconButton
51
+ label={collapsed ? expandLabel : collapseLabel}
52
+ onClick={onToggleCollapsed}
53
+ onPointerDown={stopPanelHeaderButtonPointerDown}
54
+ >
55
+ <PrimitiveArrowIcon direction={collapsed ? "down" : "up"} />
56
+ </PanelIconButton>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ );
61
+ }