@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.
- package/package.json +1 -1
- package/src/generate.mjs +34 -5
- package/src/generate.test.mjs +12 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +104 -14
- package/templates/runtime/contracts/component-contracts.ts +54 -22
- package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
- package/templates/runtime/contracts/decision-contracts.ts +3 -3
- package/templates/runtime/react/controls-panel.test.tsx +374 -13
- package/templates/runtime/react/controls-panel.tsx +65 -4
- package/templates/runtime/schema/define-toolcraft.test.ts +45 -1
- package/templates/runtime/schema/define-toolcraft.ts +25 -1
- package/templates/runtime/schema/types.ts +3 -0
- package/templates/runtime/testing/performance.test.ts +134 -0
- package/templates/runtime/testing/performance.ts +34 -4
- package/templates/starter/AGENTS.md +4 -4
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +5 -3
- package/templates/starter/docs/toolcraft/assembly-workflow.md +9 -5
- package/templates/starter/docs/toolcraft/component-rules.md +32 -11
- package/templates/starter/docs/toolcraft/performance.md +7 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +43 -14
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +2 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
- package/templates/starter/scripts/toolcraft-port.mjs +102 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +739 -66
- package/templates/starter/src/app/starter-acceptance.ts +471 -12
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +11 -3
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
- package/templates/ui/components/controls/color/index.ts +4 -1
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
- package/templates/ui/components/controls/index.ts +2 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/select/select-control.tsx +9 -4
- package/templates/ui/components/controls/slider/slider-value.ts +0 -1
- package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
- package/templates/ui/components/controls/vector/index.ts +1 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
- 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 =
|
|
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
|
|
106
|
+
className="h-fit min-w-0 gap-2"
|
|
104
107
|
data-slot="actions-control"
|
|
105
|
-
orientation="
|
|
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=
|
|
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
|
-
|
|
65
|
-
<
|
|
66
|
-
|
|
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 {
|
|
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({
|
|
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
|
|
161
|
-
|
|
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
|
-
|
|
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}
|
|
@@ -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)}
|
|
@@ -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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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,
|