@pixel-point/toolcraft 0.0.8 → 0.0.9

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 (44) hide show
  1. package/package.json +1 -1
  2. package/src/generate.mjs +34 -5
  3. package/src/generate.test.mjs +12 -0
  4. package/src/package-json.mjs +15 -0
  5. package/src/package-json.test.mjs +14 -1
  6. package/templates/runtime/contracts/component-contracts.test.ts +104 -14
  7. package/templates/runtime/contracts/component-contracts.ts +54 -22
  8. package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
  9. package/templates/runtime/contracts/decision-contracts.ts +3 -3
  10. package/templates/runtime/react/controls-panel.test.tsx +374 -13
  11. package/templates/runtime/react/controls-panel.tsx +65 -4
  12. package/templates/runtime/schema/define-toolcraft.test.ts +45 -1
  13. package/templates/runtime/schema/define-toolcraft.ts +25 -1
  14. package/templates/runtime/schema/types.ts +3 -0
  15. package/templates/runtime/testing/performance.test.ts +134 -0
  16. package/templates/runtime/testing/performance.ts +34 -4
  17. package/templates/starter/AGENTS.md +4 -4
  18. package/templates/starter/docs/toolcraft/README.md +1 -1
  19. package/templates/starter/docs/toolcraft/acceptance-testing.md +5 -3
  20. package/templates/starter/docs/toolcraft/assembly-workflow.md +9 -5
  21. package/templates/starter/docs/toolcraft/component-rules.md +32 -11
  22. package/templates/starter/docs/toolcraft/performance.md +7 -1
  23. package/templates/starter/docs/toolcraft/schema-reference.md +43 -14
  24. package/templates/starter/gitignore +1 -0
  25. package/templates/starter/package.json +2 -0
  26. package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
  27. package/templates/starter/scripts/toolcraft-port.mjs +102 -0
  28. package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
  29. package/templates/starter/src/app/starter-acceptance.test.ts +739 -66
  30. package/templates/starter/src/app/starter-acceptance.ts +471 -12
  31. package/templates/ui/components/control-layout/index.tsx +8 -3
  32. package/templates/ui/components/controls/actions/actions-control.tsx +11 -3
  33. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  34. package/templates/ui/components/controls/color/index.ts +4 -1
  35. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  36. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  37. package/templates/ui/components/controls/index.ts +2 -0
  38. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  39. package/templates/ui/components/controls/select/select-control.tsx +9 -4
  40. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  41. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  42. package/templates/ui/components/controls/vector/index.ts +1 -0
  43. package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
  44. package/templates/ui/components/panel/panel-section.tsx +29 -5
@@ -90,7 +90,7 @@ export function ControlInlineGroup({
90
90
  {...props}
91
91
  className={cn(
92
92
  "grid min-w-0",
93
- kind === "slider" ? "gap-4" : "gap-2",
93
+ kind === "slider" ? "gap-4" : "gap-x-2.5 gap-y-2",
94
94
  kind === "toggleParameter" && "items-end",
95
95
  className,
96
96
  )}
@@ -229,10 +229,13 @@ export function ControlItem({
229
229
  }: {
230
230
  allowCompoundDividers?: boolean;
231
231
  children: React.ReactNode;
232
- compoundDividerPlacement?: "both" | "bottom";
232
+ compoundDividerPlacement?: "both" | "bottom" | "top";
233
233
  flush?: boolean;
234
234
  }): React.JSX.Element {
235
- const showCompoundTopDivider = compoundDividerPlacement === "both";
235
+ const showCompoundTopDivider =
236
+ compoundDividerPlacement === "both" || compoundDividerPlacement === "top";
237
+ const showCompoundBottomDivider =
238
+ compoundDividerPlacement === "both" || compoundDividerPlacement === "bottom";
236
239
 
237
240
  return (
238
241
  <div
@@ -240,11 +243,13 @@ export function ControlItem({
240
243
  "min-w-0",
241
244
  !flush && "px-3",
242
245
  allowCompoundDividers &&
246
+ showCompoundBottomDivider &&
243
247
  "has-data-[control-section-divider=compound]:relative has-data-[control-section-divider=compound]:pb-[18px] has-data-[control-section-divider=compound]:after:absolute has-data-[control-section-divider=compound]:after:bottom-0 has-data-[control-section-divider=compound]:after:h-px has-data-[control-section-divider=compound]:after:bg-[color:color-mix(in_oklab,var(--border)_8%,transparent)]",
244
248
  allowCompoundDividers &&
245
249
  showCompoundTopDivider &&
246
250
  "has-data-[control-section-divider=compound]:pt-[18px] has-data-[control-section-divider=compound]:before:absolute has-data-[control-section-divider=compound]:before:top-0 has-data-[control-section-divider=compound]:before:h-px has-data-[control-section-divider=compound]:before:bg-[color:color-mix(in_oklab,var(--border)_8%,transparent)]",
247
251
  allowCompoundDividers &&
252
+ showCompoundBottomDivider &&
248
253
  (flush
249
254
  ? "has-data-[control-section-divider=compound]:after:inset-x-0"
250
255
  : "has-data-[control-section-divider=compound]:after:inset-x-3"),
@@ -14,6 +14,7 @@ import {
14
14
 
15
15
  import { Button, Field } from "../../primitives";
16
16
  import { ControlFieldLabel } from "../../control-layout";
17
+ import { cn } from "../../../lib/utils";
17
18
 
18
19
  export type ActionControlIconName =
19
20
  | "check"
@@ -97,12 +98,14 @@ export function ActionsControl({
97
98
  onAction,
98
99
  showLabel = true,
99
100
  }: ActionsControlProps): React.JSX.Element {
101
+ const hasSingleAction = actions.length === 1;
102
+
100
103
  return (
101
104
  <Field
102
105
  aria-label={showLabel ? undefined : name}
103
- className="h-fit min-w-0 flex-wrap items-center justify-between gap-y-2!"
106
+ className="h-fit min-w-0 gap-2"
104
107
  data-slot="actions-control"
105
- orientation="horizontal"
108
+ orientation="vertical"
106
109
  >
107
110
  {showLabel ? (
108
111
  <ControlFieldLabel className="min-w-0" textClassName="min-w-0 truncate">
@@ -110,7 +113,11 @@ export function ActionsControl({
110
113
  </ControlFieldLabel>
111
114
  ) : null}
112
115
  <div
113
- className="flex max-w-full shrink-0 flex-wrap items-center justify-start gap-1.5"
116
+ className={cn(
117
+ "grid max-w-full gap-1.5",
118
+ hasSingleAction ? "w-1/2 grid-cols-1" : "w-full grid-cols-2",
119
+ )}
120
+ data-actions-count={actions.length}
114
121
  data-slot="actions-control-buttons"
115
122
  >
116
123
  {actions.map((action) => {
@@ -122,6 +129,7 @@ export function ActionsControl({
122
129
  aria-label={getActionAriaLabel(action, actionContent)}
123
130
  key={actionValue}
124
131
  onClick={() => onAction?.(actionValue)}
132
+ className="w-full"
125
133
  size="sm"
126
134
  type="button"
127
135
  variant="outline"
@@ -14,12 +14,14 @@ export type CodeTextareaControlProps = {
14
14
  defaultValue?: string;
15
15
  name: string;
16
16
  onValueChange?: ControlValueChangeHandler<string>;
17
+ showLabel?: boolean;
17
18
  value: string;
18
19
  };
19
20
 
20
21
  export function CodeTextareaControl({
21
22
  name,
22
23
  onValueChange,
24
+ showLabel = true,
23
25
  value,
24
26
  }: CodeTextareaControlProps): React.JSX.Element {
25
27
  const [currentValue, setCurrentValue] = React.useState(value);
@@ -61,9 +63,11 @@ export function CodeTextareaControl({
61
63
 
62
64
  return (
63
65
  <Field className="min-w-0 gap-2">
64
- <div className="flex items-center">
65
- <ControlFieldLabel>{name}</ControlFieldLabel>
66
- </div>
66
+ {showLabel ? (
67
+ <div className="flex items-center">
68
+ <ControlFieldLabel>{name}</ControlFieldLabel>
69
+ </div>
70
+ ) : null}
67
71
  <Textarea
68
72
  aria-label={name}
69
73
  className="max-h-[calc(12lh+12px)] min-h-[84px] overflow-y-auto font-mono"
@@ -18,7 +18,10 @@ export {
18
18
  type ColorFormatMode,
19
19
  type ColorSurfaceModel,
20
20
  } from "./style-guide-color-picker-channel-utils";
21
- export { DEFAULT_COLOR_FORMAT_MODE } from "./style-guide-color-picker";
21
+ export {
22
+ DEFAULT_COLOR_FORMAT_MODE,
23
+ StyleGuideColorPicker,
24
+ } from "./style-guide-color-picker";
22
25
  export {
23
26
  getColorSurfaceSliderConfig,
24
27
  getColorSurfaceStyle,
@@ -459,9 +459,14 @@ export function useHueHandlers({
459
459
 
460
460
  const handleHuePreviewChange = useCallback(
461
461
  (nextHue: number) => {
462
- applyOptimisticColor({ h: nextHue, s: latestHsvRef.current.s, v: latestHsvRef.current.v });
462
+ const nextHex = applyOptimisticColor({
463
+ h: nextHue,
464
+ s: latestHsvRef.current.s,
465
+ v: latestHsvRef.current.v,
466
+ });
467
+ emitChange(nextHex);
463
468
  },
464
- [applyOptimisticColor, latestHsvRef],
469
+ [applyOptimisticColor, emitChange, latestHsvRef],
465
470
  );
466
471
 
467
472
  const handleHueCommit = useCallback(
@@ -170,9 +170,9 @@ function useRgbBlueHandlers({
170
170
 
171
171
  const handleSliderPreviewChange = useCallback(
172
172
  (nextValue: number) => {
173
- applyBlueChannel(nextValue);
173
+ emitChange(applyBlueChannel(nextValue));
174
174
  },
175
- [applyBlueChannel],
175
+ [applyBlueChannel, emitChange],
176
176
  );
177
177
 
178
178
  const handleSliderCommit = useCallback(
@@ -52,6 +52,7 @@ export {
52
52
  getSurfaceHsvColor,
53
53
  PaletteControl,
54
54
  PaletteControl as Palette,
55
+ StyleGuideColorPicker,
55
56
  } from "./color";
56
57
  export type {
57
58
  ColorFormatMode,
@@ -197,5 +198,6 @@ export type {
197
198
  VectorControlProps as VectorProps,
198
199
  VectorControlValue,
199
200
  VectorControlValue as VectorValue,
201
+ VectorPadCoordinateMode,
200
202
  VectorPadVariant,
201
203
  } from "./vector";
@@ -12,6 +12,7 @@ export type RangeInputControlProps = {
12
12
  end: string;
13
13
  name: string;
14
14
  onValueChange?: ControlValueChangeHandler<{ start: string; end: string }>;
15
+ showLabel?: boolean;
15
16
  start: string;
16
17
  };
17
18
 
@@ -88,6 +89,7 @@ export function RangeInputControl({
88
89
  end,
89
90
  name,
90
91
  onValueChange,
92
+ showLabel = true,
91
93
  start,
92
94
  }: RangeInputControlProps): React.JSX.Element {
93
95
  const [currentValue, setCurrentValue] = React.useState({ end, start });
@@ -142,7 +144,7 @@ export function RangeInputControl({
142
144
  if (shouldUseFullWidthRangeInputs(start, end)) {
143
145
  return (
144
146
  <Field className="h-fit min-w-0 gap-2">
145
- <ControlFieldLabel>{name}</ControlFieldLabel>
147
+ {showLabel ? <ControlFieldLabel>{name}</ControlFieldLabel> : null}
146
148
  <RangeInputs
147
149
  className="w-full"
148
150
  end={currentValue.end}
@@ -157,10 +159,16 @@ export function RangeInputControl({
157
159
  }
158
160
 
159
161
  return (
160
- <Field className="h-fit min-w-0 items-center justify-between gap-3" orientation="horizontal">
161
- <ControlFieldLabel>{name}</ControlFieldLabel>
162
+ <Field
163
+ className={cn(
164
+ "h-fit min-w-0",
165
+ showLabel ? "items-center justify-between gap-3" : undefined,
166
+ )}
167
+ orientation={showLabel ? "horizontal" : "vertical"}
168
+ >
169
+ {showLabel ? <ControlFieldLabel>{name}</ControlFieldLabel> : null}
162
170
  <RangeInputs
163
- className="w-1/2 shrink-0"
171
+ className={showLabel ? "w-1/2 shrink-0" : "w-full"}
164
172
  end={currentValue.end}
165
173
  name={name}
166
174
  onCancel={cancelDraft}
@@ -21,6 +21,7 @@ export type SelectControlInput = {
21
21
  name: string;
22
22
  onValueChange?: (value: string) => void;
23
23
  options: readonly ControlOption[];
24
+ showLabel?: boolean;
24
25
  value: string;
25
26
  };
26
27
 
@@ -145,6 +146,7 @@ function SelectControlField({
145
146
  name,
146
147
  onValueChange,
147
148
  options,
149
+ showLabel = true,
148
150
  value,
149
151
  }: SelectControlInput & {
150
152
  layout?: SelectControlFieldLayout;
@@ -169,21 +171,24 @@ function SelectControlField({
169
171
  return (
170
172
  <Field
171
173
  className={cn(
172
- isStacked
174
+ !showLabel
175
+ ? "h-fit min-w-0"
176
+ : isStacked
173
177
  ? "h-fit min-w-0 gap-2"
174
178
  : compactHorizontalFieldClassName,
175
179
  )}
176
- orientation={isStacked ? "vertical" : "horizontal"}
180
+ orientation={!showLabel || isStacked ? "vertical" : "horizontal"}
177
181
  ref={fieldRef}
178
182
  >
179
- <ControlFieldLabel>{name}</ControlFieldLabel>
183
+ {showLabel ? <ControlFieldLabel>{name}</ControlFieldLabel> : null}
180
184
  <div
181
185
  className={cn(
182
186
  "min-w-0",
183
- isStacked ? "w-full" : "w-1/2 shrink-0",
187
+ !showLabel || isStacked ? "w-full" : "w-1/2 shrink-0",
184
188
  )}
185
189
  >
186
190
  <StaticSelect
191
+ ariaLabel={showLabel ? undefined : name}
187
192
  onValueChange={updateValue}
188
193
  options={options}
189
194
  popupMaxWidth={popupMaxWidth}
@@ -27,7 +27,6 @@ const compactSliderValueUnits = new Set([
27
27
  "vh",
28
28
  "vmin",
29
29
  "vmax",
30
- "x",
31
30
  "s",
32
31
  "ms",
33
32
  ]);
@@ -15,6 +15,7 @@ export type TextInputControlInput = {
15
15
  defaultValue?: string;
16
16
  name: string;
17
17
  onValueChange?: ControlValueChangeHandler<string>;
18
+ showLabel?: boolean;
18
19
  value: string;
19
20
  };
20
21
 
@@ -51,6 +52,7 @@ function TextInputControlField({
51
52
  defaultValue,
52
53
  name,
53
54
  onValueChange,
55
+ showLabel = true,
54
56
  value,
55
57
  }: TextInputControlInput): React.JSX.Element {
56
58
  const [currentValue, setCurrentValue] = React.useState(value);
@@ -101,8 +103,9 @@ function TextInputControlField({
101
103
 
102
104
  return (
103
105
  <Field className="min-w-0 gap-2">
104
- <ControlFieldLabel>{name}</ControlFieldLabel>
106
+ {showLabel ? <ControlFieldLabel>{name}</ControlFieldLabel> : null}
105
107
  <Input
108
+ aria-label={showLabel ? undefined : name}
106
109
  className="font-mono"
107
110
  onBlur={commitOnBlur ? () => commitValue() : finishLiveHistoryGroup}
108
111
  onChange={(event) => updateValue(event.target.value)}
@@ -4,5 +4,6 @@ export { VectorControl } from "./vector-control";
4
4
  export type {
5
5
  VectorControlProps,
6
6
  VectorControlValue,
7
+ VectorPadCoordinateMode,
7
8
  VectorPadVariant,
8
9
  } from "./vector-control";
@@ -17,6 +17,14 @@ export type VectorControlValue = {
17
17
  y: string;
18
18
  };
19
19
 
20
+ type VectorPadAxisLock = "x" | "y";
21
+
22
+ type VectorPadDragStart = {
23
+ clientX: number;
24
+ clientY: number;
25
+ value: VectorControlValue;
26
+ };
27
+
20
28
  export type VectorPadVariant =
21
29
  | "default"
22
30
  | "whiteBalance"
@@ -24,10 +32,13 @@ export type VectorPadVariant =
24
32
  | "chromaOffset"
25
33
  | "toneBias";
26
34
 
35
+ export type VectorPadCoordinateMode = "cartesian" | "screen";
36
+
27
37
  export type VectorControlProps = VectorControlValue & {
28
38
  defaultValue?: Partial<VectorControlValue>;
29
39
  name: string;
30
40
  onValueChange?: ControlValueChangeHandler<VectorControlValue>;
41
+ padCoordinateMode?: VectorPadCoordinateMode;
31
42
  padShape?: "compact" | "square";
32
43
  padVariant?: VectorPadVariant;
33
44
  xLabel?: string;
@@ -38,10 +49,20 @@ function clamp(value: number, min = 0, max = 1): number {
38
49
  return Math.min(max, Math.max(min, value));
39
50
  }
40
51
 
41
- function pointFromEvent(event: React.PointerEvent<HTMLElement>): CurvePoint {
52
+ function getDefaultPadCoordinateMode(
53
+ padVariant: VectorPadVariant,
54
+ ): VectorPadCoordinateMode {
55
+ return padVariant === "default" ? "screen" : "cartesian";
56
+ }
57
+
58
+ function pointFromEvent(
59
+ event: React.PointerEvent<HTMLElement>,
60
+ coordinateMode: VectorPadCoordinateMode,
61
+ ): CurvePoint {
42
62
  const rect = event.currentTarget.getBoundingClientRect();
43
63
  const x = clamp((event.clientX - rect.left) / rect.width);
44
- const y = clamp(1 - (event.clientY - rect.top) / rect.height);
64
+ const yRatio = clamp((event.clientY - rect.top) / rect.height);
65
+ const y = coordinateMode === "screen" ? yRatio : 1 - yRatio;
45
66
 
46
67
  return { x, y };
47
68
  }
@@ -54,6 +75,8 @@ type VectorPadStyle = React.CSSProperties & {
54
75
  "--xy-pad-y": string;
55
76
  };
56
77
 
78
+ const vectorPadAxisLockThresholdPx = 2;
79
+
57
80
  const whiteBalancePadBackgroundImage =
58
81
  "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
82
 
@@ -73,13 +96,20 @@ const vectorPadBackgroundImages: Partial<Record<VectorPadVariant, string>> = {
73
96
  whiteBalance: whiteBalancePadBackgroundImage,
74
97
  };
75
98
 
76
- function getVectorPoint(x: string, y: string): VectorPadStyle {
99
+ function getVectorPoint(
100
+ x: string,
101
+ y: string,
102
+ coordinateMode: VectorPadCoordinateMode,
103
+ ): VectorPadStyle {
77
104
  const parsedX = Number.parseFloat(x);
78
105
  const parsedY = Number.parseFloat(y);
79
106
  const clampedX = Number.isFinite(parsedX) ? clamp(parsedX, -1, 1) : 0;
80
107
  const clampedY = Number.isFinite(parsedY) ? clamp(parsedY, -1, 1) : 0;
81
108
  const xPosition = `${(clampedX + 1) * 50}%`;
82
- const yPosition = `${(1 - (clampedY + 1) / 2) * 100}%`;
109
+ const yPosition =
110
+ coordinateMode === "screen"
111
+ ? `${((clampedY + 1) / 2) * 100}%`
112
+ : `${(1 - (clampedY + 1) / 2) * 100}%`;
83
113
 
84
114
  return {
85
115
  "--xy-pad-display-x":
@@ -279,6 +309,7 @@ function VectorPadHandle({ isDragging }: { isDragging: boolean }): React.JSX.Ele
279
309
  function VectorPadField({
280
310
  name,
281
311
  onValueChange,
312
+ padCoordinateMode,
282
313
  padShape = "compact",
283
314
  padVariant = "default",
284
315
  x,
@@ -287,9 +318,13 @@ function VectorPadField({
287
318
  const [isPointerDragging, setIsPointerDragging] = React.useState(false);
288
319
  const normalizedX = normalizeVectorCoordinate(x);
289
320
  const normalizedY = normalizeVectorCoordinate(y);
290
- const point = getVectorPoint(normalizedX, normalizedY);
321
+ const coordinateMode =
322
+ padCoordinateMode ?? getDefaultPadCoordinateMode(padVariant);
323
+ const point = getVectorPoint(normalizedX, normalizedY, coordinateMode);
291
324
  const valueLabel = getVectorValueLabel(normalizedX, normalizedY);
292
325
  const vectorPadBackgroundImage = vectorPadBackgroundImages[padVariant];
326
+ const axisLockRef = React.useRef<VectorPadAxisLock | null>(null);
327
+ const dragStartRef = React.useRef<VectorPadDragStart | null>(null);
293
328
  const liveHistoryGroupRef = React.useRef<string | null>(null);
294
329
  const updateVector = (
295
330
  nextX: string,
@@ -321,17 +356,51 @@ function VectorPadField({
321
356
  }
322
357
 
323
358
  function updateFromPointer(event: React.PointerEvent<HTMLButtonElement>): void {
324
- const nextPoint = pointFromEvent(event);
359
+ const nextPoint = pointFromEvent(event, coordinateMode);
360
+ const nextValue: VectorControlValue = {
361
+ x: (nextPoint.x * 2 - 1).toFixed(2),
362
+ y: (nextPoint.y * 2 - 1).toFixed(2),
363
+ };
364
+ const dragStart = dragStartRef.current;
365
+
366
+ if (event.shiftKey && dragStart) {
367
+ if (!axisLockRef.current) {
368
+ const deltaX = event.clientX - dragStart.clientX;
369
+ const deltaY = event.clientY - dragStart.clientY;
370
+
371
+ if (Math.hypot(deltaX, deltaY) < vectorPadAxisLockThresholdPx) {
372
+ updateVector(
373
+ dragStart.value.x,
374
+ dragStart.value.y,
375
+ getLiveHistoryMeta(),
376
+ );
377
+ return;
378
+ }
379
+
380
+ axisLockRef.current = Math.abs(deltaX) >= Math.abs(deltaY) ? "x" : "y";
381
+ }
382
+
383
+ updateVector(
384
+ axisLockRef.current === "x" ? nextValue.x : dragStart.value.x,
385
+ axisLockRef.current === "y" ? nextValue.y : dragStart.value.y,
386
+ getLiveHistoryMeta(),
387
+ );
388
+ return;
389
+ }
390
+
391
+ axisLockRef.current = null;
325
392
 
326
393
  updateVector(
327
- (nextPoint.x * 2 - 1).toFixed(2),
328
- (nextPoint.y * 2 - 1).toFixed(2),
394
+ nextValue.x,
395
+ nextValue.y,
329
396
  getLiveHistoryMeta(),
330
397
  );
331
398
  }
332
399
 
333
400
  function stopPointerDrag(): void {
334
401
  setIsPointerDragging(false);
402
+ axisLockRef.current = null;
403
+ dragStartRef.current = null;
335
404
  liveHistoryGroupRef.current = null;
336
405
  }
337
406
 
@@ -352,12 +421,19 @@ function VectorPadField({
352
421
  "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
422
  padShape === "square" ? "aspect-square" : "h-[142px]",
354
423
  )}
424
+ data-vector-pad-coordinate-mode={coordinateMode}
355
425
  data-vector-pad-shape={padShape}
356
426
  data-vector-pad-variant={padVariant}
357
427
  onLostPointerCapture={stopPointerDrag}
358
428
  onPointerCancel={stopPointerDrag}
359
429
  onPointerDown={(event) => {
360
430
  setIsPointerDragging(true);
431
+ axisLockRef.current = null;
432
+ dragStartRef.current = {
433
+ clientX: event.clientX,
434
+ clientY: event.clientY,
435
+ value: { x: normalizedX, y: normalizedY },
436
+ };
361
437
  event.currentTarget.setPointerCapture(event.pointerId);
362
438
  updateFromPointer(event);
363
439
  }}
@@ -53,11 +53,11 @@ export function PanelSection({
53
53
  {childArray.map((child, index) => (
54
54
  <ControlItem
55
55
  allowCompoundDividers={shouldRenderInnerDividers}
56
- compoundDividerPlacement={
57
- shouldRenderInnerDividers && childArray.length > 1 && index === 0
58
- ? "bottom"
59
- : "both"
60
- }
56
+ compoundDividerPlacement={getCompoundDividerPlacement({
57
+ childCount: childArray.length,
58
+ index,
59
+ shouldRenderInnerDividers,
60
+ })}
61
61
  flush={flush || isActionSection}
62
62
  key={getPanelSectionChildKey(child, index)}
63
63
  >
@@ -100,6 +100,30 @@ export function PanelSection({
100
100
  );
101
101
  }
102
102
 
103
+ function getCompoundDividerPlacement({
104
+ childCount,
105
+ index,
106
+ shouldRenderInnerDividers,
107
+ }: {
108
+ childCount: number;
109
+ index: number;
110
+ shouldRenderInnerDividers: boolean;
111
+ }): "both" | "bottom" | "top" {
112
+ if (!shouldRenderInnerDividers || childCount <= 1) {
113
+ return "both";
114
+ }
115
+
116
+ if (index === 0) {
117
+ return "bottom";
118
+ }
119
+
120
+ if (index === childCount - 1) {
121
+ return "top";
122
+ }
123
+
124
+ return "both";
125
+ }
126
+
103
127
  function PanelSectionCollapsibleBody({
104
128
  collapsed,
105
129
  children,