@pixel-point/toolcraft 0.0.8 → 0.0.11
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/README.md +42 -9
- package/package.json +1 -1
- package/src/generate.mjs +42 -5
- package/src/generate.test.mjs +40 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +251 -47
- package/templates/runtime/contracts/component-contracts.ts +130 -57
- package/templates/runtime/contracts/decision-contracts.test.ts +38 -1
- package/templates/runtime/contracts/decision-contracts.ts +22 -9
- package/templates/runtime/export/export.test.ts +63 -0
- package/templates/runtime/export/export.ts +55 -0
- package/templates/runtime/index.ts +1 -0
- package/templates/runtime/react/canvas-shell.test.tsx +58 -4
- package/templates/runtime/react/canvas-shell.tsx +31 -9
- package/templates/runtime/react/controls-panel.test.tsx +916 -108
- package/templates/runtime/react/controls-panel.tsx +136 -30
- package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
- package/templates/runtime/react/settings-transfer.test.ts +4 -0
- package/templates/runtime/react/settings-transfer.ts +6 -1
- package/templates/runtime/react/timeline-panel.test.tsx +14 -0
- package/templates/runtime/react/timeline-panel.tsx +44 -7
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
- package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
- package/templates/runtime/react/toolcraft-app.tsx +56 -37
- package/templates/runtime/schema/define-toolcraft.test.ts +266 -170
- package/templates/runtime/schema/define-toolcraft.ts +140 -246
- package/templates/runtime/schema/runtime-targets.ts +21 -0
- package/templates/runtime/schema/types.ts +44 -0
- package/templates/runtime/state/create-template-state.test.ts +156 -0
- package/templates/runtime/state/create-template-state.ts +38 -8
- package/templates/runtime/state/media-defaults.ts +105 -0
- package/templates/runtime/state/persistence.test.ts +58 -0
- package/templates/runtime/state/persistence.ts +105 -1
- package/templates/runtime/state/reducer.test.ts +280 -4
- package/templates/runtime/state/reducer.ts +195 -9
- package/templates/runtime/state/timeline-loop.test.ts +71 -0
- package/templates/runtime/state/timeline-loop.ts +35 -0
- package/templates/runtime/state/types.ts +27 -0
- package/templates/runtime/testing/performance.test.ts +810 -21
- package/templates/runtime/testing/performance.ts +823 -53
- package/templates/starter/AGENTS.md +24 -18
- package/templates/starter/docs/toolcraft/README.md +8 -4
- package/templates/starter/docs/toolcraft/acceptance-testing.md +43 -10
- package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
- package/templates/starter/docs/toolcraft/assembly-workflow.md +55 -20
- package/templates/starter/docs/toolcraft/component-rules.md +75 -42
- package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
- package/templates/starter/docs/toolcraft/performance.md +37 -11
- package/templates/starter/docs/toolcraft/schema-reference.md +171 -41
- package/templates/starter/docs/toolcraft/workflow.md +5 -2
- package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
- package/templates/starter/e2e/app-performance.spec.ts +55 -2
- package/templates/starter/e2e/performance-helpers.ts +45 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/index.html +1 -0
- package/templates/starter/package.json +3 -0
- package/templates/starter/playwright.config.ts +1 -1
- package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +114 -13
- package/templates/starter/scripts/toolcraft-port.mjs +280 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +207 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +3412 -479
- package/templates/starter/src/app/starter-acceptance.ts +1453 -97
- package/templates/starter/src/app/starter-performance.test.ts +111 -7
- package/templates/starter/src/app/starter-performance.ts +5 -0
- package/templates/starter/src/app/starter-schema.test.ts +32 -7
- package/templates/starter/src/app/starter-schema.ts +6 -2
- package/templates/starter/vite.config.ts +58 -2
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +56 -5
- 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/palette-control.tsx +34 -4
- 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/file-drop/file-drop-control.tsx +186 -14
- package/templates/ui/components/controls/file-drop/index.ts +6 -1
- package/templates/ui/components/controls/index.ts +4 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
- package/templates/ui/components/controls/select/select-control.tsx +8 -25
- 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 +109 -12
- package/templates/ui/components/panel/panel-actions.tsx +1 -1
- package/templates/ui/components/panel/panel-section.tsx +29 -5
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
type GradientStop,
|
|
48
48
|
type GradientType,
|
|
49
49
|
type ImagePickerItem,
|
|
50
|
+
type VectorPadCoordinateMode,
|
|
50
51
|
type VectorPadVariant,
|
|
51
52
|
} from "@repo/ui";
|
|
52
53
|
|
|
@@ -64,7 +65,12 @@ import {
|
|
|
64
65
|
toolcraftCanvasAspectRatioPresets,
|
|
65
66
|
} from "../schema/canvas-aspect-ratio-presets";
|
|
66
67
|
import { getToolcraftControlKeyframeCapability } from "../schema/keyframe-capability";
|
|
67
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
getToolcraftCanvasSizeTargetDimension,
|
|
70
|
+
isToolcraftRuntimeOwnedTarget,
|
|
71
|
+
isToolcraftTimelinePanelExtendedTarget,
|
|
72
|
+
isToolcraftTimelinePanelVisibleTarget,
|
|
73
|
+
} from "../schema/runtime-targets";
|
|
68
74
|
import type {
|
|
69
75
|
ToolcraftCommand,
|
|
70
76
|
ToolcraftPanelState,
|
|
@@ -421,6 +427,17 @@ function asVectorPadVariant(value: string | undefined): VectorPadVariant {
|
|
|
421
427
|
return "default";
|
|
422
428
|
}
|
|
423
429
|
|
|
430
|
+
function asVectorPadCoordinateMode(
|
|
431
|
+
value: string | undefined,
|
|
432
|
+
padVariant: VectorPadVariant,
|
|
433
|
+
): VectorPadCoordinateMode {
|
|
434
|
+
if (value === "cartesian" || value === "screen") {
|
|
435
|
+
return value;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return padVariant === "default" ? "screen" : "cartesian";
|
|
439
|
+
}
|
|
440
|
+
|
|
424
441
|
function asCurveInterpolation(value: string | undefined): CurveInterpolation | undefined {
|
|
425
442
|
return value === "monotone" || value === "smooth" ? value : undefined;
|
|
426
443
|
}
|
|
@@ -773,7 +790,6 @@ function CanvasAspectRatioControl({
|
|
|
773
790
|
return (
|
|
774
791
|
<div className="min-w-0 space-y-2" data-slot="canvas-aspect-ratio-control">
|
|
775
792
|
<Select
|
|
776
|
-
layout="stacked"
|
|
777
793
|
name={name}
|
|
778
794
|
onValueChange={updatePreset}
|
|
779
795
|
options={canvasAspectRatioOptions}
|
|
@@ -838,6 +854,23 @@ function getActionLabel(action: ToolcraftActionSchema): string {
|
|
|
838
854
|
return action.label ?? action.value;
|
|
839
855
|
}
|
|
840
856
|
|
|
857
|
+
function isExportPanelAction(action: ToolcraftActionSchema): boolean {
|
|
858
|
+
const label = getActionLabel(action);
|
|
859
|
+
const value = action.value;
|
|
860
|
+
|
|
861
|
+
return (
|
|
862
|
+
/\bexport\b/i.test(label) ||
|
|
863
|
+
/(?:^|[._:-])export(?:[._:-]|$)/i.test(value) ||
|
|
864
|
+
/^export(?:[._:-]|$)/i.test(value)
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function getPanelActionIcon(
|
|
869
|
+
action: ToolcraftActionSchema,
|
|
870
|
+
): PanelActionObjectOption["icon"] {
|
|
871
|
+
return isExportPanelAction(action) ? "upload-simple" : action.icon;
|
|
872
|
+
}
|
|
873
|
+
|
|
841
874
|
function getControlName(id: string, label: boolean | string | undefined): string {
|
|
842
875
|
if (typeof label === "string") {
|
|
843
876
|
return label;
|
|
@@ -974,26 +1007,19 @@ function isColorOnlySection(entries: readonly ControlEntry[]): boolean {
|
|
|
974
1007
|
function getRenderedControlsSectionTitle(
|
|
975
1008
|
section: ToolcraftControlSectionSchema,
|
|
976
1009
|
): ToolcraftControlSectionSchema["title"] {
|
|
977
|
-
return
|
|
1010
|
+
return isStickyFooterActionSection(section) ? undefined : section.title;
|
|
978
1011
|
}
|
|
979
1012
|
|
|
980
|
-
function
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
return Object.values(section.controls).some(
|
|
992
|
-
(control) =>
|
|
993
|
-
control.type === "settingsTransfer" ||
|
|
994
|
-
control.target === "canvas.aspectRatio" ||
|
|
995
|
-
control.target === "canvas.size.width" ||
|
|
996
|
-
control.target === "canvas.size.height",
|
|
1013
|
+
function isRuntimeSetupSection({
|
|
1014
|
+
entries,
|
|
1015
|
+
section,
|
|
1016
|
+
}: {
|
|
1017
|
+
entries: readonly ControlEntry[];
|
|
1018
|
+
section: ToolcraftControlSectionSchema;
|
|
1019
|
+
}): boolean {
|
|
1020
|
+
return (
|
|
1021
|
+
section.title === "Setup" &&
|
|
1022
|
+
entries.some(([, control]) => isToolcraftRuntimeOwnedTarget(control.target))
|
|
997
1023
|
);
|
|
998
1024
|
}
|
|
999
1025
|
|
|
@@ -1154,6 +1180,15 @@ function hasSliderControl(
|
|
|
1154
1180
|
);
|
|
1155
1181
|
}
|
|
1156
1182
|
|
|
1183
|
+
function hasSegmentedControl(
|
|
1184
|
+
controlsById: Record<string, ToolcraftControlSchema>,
|
|
1185
|
+
layoutGroup: ToolcraftControlLayoutGroupSchema,
|
|
1186
|
+
): boolean {
|
|
1187
|
+
return layoutGroup.controls.some(
|
|
1188
|
+
(controlId) => controlsById[controlId]?.type === "segmented",
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1157
1192
|
function hasSectionedCompoundControl(
|
|
1158
1193
|
controlsById: Record<string, ToolcraftControlSchema>,
|
|
1159
1194
|
layoutGroup: ToolcraftControlLayoutGroupSchema,
|
|
@@ -1214,6 +1249,22 @@ function isToggleParameterLayoutGroup(
|
|
|
1214
1249
|
return booleanControlCount === 1 && parameterControlCount === 1;
|
|
1215
1250
|
}
|
|
1216
1251
|
|
|
1252
|
+
function shouldHideToggleParameterControlLabel({
|
|
1253
|
+
control,
|
|
1254
|
+
controlsById,
|
|
1255
|
+
layoutGroup,
|
|
1256
|
+
}: {
|
|
1257
|
+
control: ToolcraftControlSchema;
|
|
1258
|
+
controlsById: Record<string, ToolcraftControlSchema>;
|
|
1259
|
+
layoutGroup: ToolcraftControlLayoutGroupSchema | undefined;
|
|
1260
|
+
}): boolean {
|
|
1261
|
+
return Boolean(
|
|
1262
|
+
layoutGroup &&
|
|
1263
|
+
isToggleParameterLayoutGroup(controlsById, layoutGroup) &&
|
|
1264
|
+
!isBooleanControl(control),
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1217
1268
|
function getControlTargetEntityKey(control: ToolcraftControlSchema): string | null {
|
|
1218
1269
|
const parts = control.target.split(".");
|
|
1219
1270
|
|
|
@@ -1409,6 +1460,7 @@ function getInlineLayoutGroupByControlId({
|
|
|
1409
1460
|
if (
|
|
1410
1461
|
hasColorOpacityControl(controlsById, layoutGroup) ||
|
|
1411
1462
|
hasSliderControl(controlsById, layoutGroup) ||
|
|
1463
|
+
hasSegmentedControl(controlsById, layoutGroup) ||
|
|
1412
1464
|
hasSectionedCompoundControl(controlsById, layoutGroup) ||
|
|
1413
1465
|
hasUnsafeInlineSwitchLabels(controlsById, layoutGroup)
|
|
1414
1466
|
) {
|
|
@@ -1576,6 +1628,14 @@ export function ControlsPanel({
|
|
|
1576
1628
|
function getControlValue(control: ToolcraftControlSchema): unknown {
|
|
1577
1629
|
const canvasSizeDimension = getToolcraftCanvasSizeTargetDimension(control.target);
|
|
1578
1630
|
|
|
1631
|
+
if (isToolcraftTimelinePanelExtendedTarget(control.target)) {
|
|
1632
|
+
return state.panels.timeline.extended === true;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
if (isToolcraftTimelinePanelVisibleTarget(control.target)) {
|
|
1636
|
+
return state.panels.timeline.hidden !== true;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1579
1639
|
return canvasSizeDimension
|
|
1580
1640
|
? state.canvas.size[canvasSizeDimension]
|
|
1581
1641
|
: (state.values[control.target] ?? control.defaultValue);
|
|
@@ -1596,6 +1656,14 @@ export function ControlsPanel({
|
|
|
1596
1656
|
function getTargetValue(target: string): unknown {
|
|
1597
1657
|
const canvasSizeDimension = getToolcraftCanvasSizeTargetDimension(target);
|
|
1598
1658
|
|
|
1659
|
+
if (isToolcraftTimelinePanelExtendedTarget(target)) {
|
|
1660
|
+
return state.panels.timeline.extended === true;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
if (isToolcraftTimelinePanelVisibleTarget(target)) {
|
|
1664
|
+
return state.panels.timeline.hidden !== true;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1599
1667
|
return canvasSizeDimension
|
|
1600
1668
|
? state.canvas.size[canvasSizeDimension]
|
|
1601
1669
|
: (state.values[target] ?? getControlDefaultValueByTarget(target));
|
|
@@ -1893,6 +1961,10 @@ export function ControlsPanel({
|
|
|
1893
1961
|
providerKey: string;
|
|
1894
1962
|
sectionTitle: string | undefined;
|
|
1895
1963
|
}): React.ReactNode {
|
|
1964
|
+
if (control.label === false) {
|
|
1965
|
+
return children;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1896
1968
|
const help = getControlHelpText({ control, label, sectionTitle });
|
|
1897
1969
|
|
|
1898
1970
|
if (!help) {
|
|
@@ -2329,14 +2401,17 @@ export function ControlsPanel({
|
|
|
2329
2401
|
{visibleSections.map(({ entries, section }, sectionIndex) => {
|
|
2330
2402
|
const visibleControls = getControlsRecord(entries);
|
|
2331
2403
|
const sectionHasOnlyColorFields = isColorOnlySection(entries);
|
|
2332
|
-
const
|
|
2333
|
-
const
|
|
2404
|
+
const isRuntimeSetup = isRuntimeSetupSection({ entries, section });
|
|
2405
|
+
const renderedSectionTitle = isRuntimeSetup
|
|
2406
|
+
? undefined
|
|
2407
|
+
: getRenderedControlsSectionTitle(section);
|
|
2408
|
+
const sectionSpacing = "default";
|
|
2334
2409
|
const sectionCollapseKey = getControlsPanelSectionCollapseKey({
|
|
2335
2410
|
entries,
|
|
2336
2411
|
section,
|
|
2337
2412
|
sectionIndex,
|
|
2338
2413
|
});
|
|
2339
|
-
const isSectionCollapsible = renderedSectionTitle !== undefined;
|
|
2414
|
+
const isSectionCollapsible = !isRuntimeSetup && renderedSectionTitle !== undefined;
|
|
2340
2415
|
const isSectionCollapsed =
|
|
2341
2416
|
isSectionCollapsible && collapsedSectionByKey[sectionCollapseKey] === true;
|
|
2342
2417
|
const headerKeyframeEntry = getSectionHeaderKeyframeEntry(entries, section.title);
|
|
@@ -2404,13 +2479,23 @@ export function ControlsPanel({
|
|
|
2404
2479
|
}
|
|
2405
2480
|
|
|
2406
2481
|
const [id, rawControl] = group.entry;
|
|
2407
|
-
const
|
|
2482
|
+
const inlineLayoutGroup = inlineLayoutGroupByControlId.get(id);
|
|
2483
|
+
const isInInlineLayoutGroup = Boolean(inlineLayoutGroup);
|
|
2408
2484
|
const disabled = isControlDisabled(rawControl);
|
|
2409
|
-
const
|
|
2485
|
+
const resolvedControl =
|
|
2410
2486
|
disabled === Boolean(rawControl.disabled)
|
|
2411
2487
|
? rawControl
|
|
2412
2488
|
: { ...rawControl, disabled };
|
|
2413
|
-
const
|
|
2489
|
+
const shouldHideLabel = shouldHideToggleParameterControlLabel({
|
|
2490
|
+
control: resolvedControl,
|
|
2491
|
+
controlsById: visibleControls,
|
|
2492
|
+
layoutGroup: inlineLayoutGroup,
|
|
2493
|
+
});
|
|
2494
|
+
const control =
|
|
2495
|
+
shouldHideLabel && resolvedControl.label !== false
|
|
2496
|
+
? { ...resolvedControl, label: false }
|
|
2497
|
+
: resolvedControl;
|
|
2498
|
+
const name = getControlName(id, resolvedControl.label);
|
|
2414
2499
|
const value = getControlValue(control);
|
|
2415
2500
|
const usesHeaderKeyframeAction = control.target === headerKeyframeTarget;
|
|
2416
2501
|
const commitWithLabel =
|
|
@@ -2522,6 +2607,7 @@ export function ControlsPanel({
|
|
|
2522
2607
|
key={id}
|
|
2523
2608
|
name={name}
|
|
2524
2609
|
onValueChange={commit}
|
|
2610
|
+
showLabel={control.label !== false}
|
|
2525
2611
|
value={asString(value)}
|
|
2526
2612
|
/>
|
|
2527
2613
|
),
|
|
@@ -2624,6 +2710,7 @@ export function ControlsPanel({
|
|
|
2624
2710
|
id: asset.id,
|
|
2625
2711
|
size: asset.size,
|
|
2626
2712
|
src: asset.dataUrl,
|
|
2713
|
+
transform: asset.transform,
|
|
2627
2714
|
}));
|
|
2628
2715
|
const previewMediaIds = previewMediaAssets.map((asset) => asset.id);
|
|
2629
2716
|
const importFile = (file: File, replaceExisting: boolean): void => {
|
|
@@ -2733,6 +2820,17 @@ export function ControlsPanel({
|
|
|
2733
2820
|
type: "media.reorder",
|
|
2734
2821
|
});
|
|
2735
2822
|
}}
|
|
2823
|
+
onPreviewTransform={(item, operation) => {
|
|
2824
|
+
if (!item.id) {
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
dispatchCommand({
|
|
2829
|
+
mediaId: item.id,
|
|
2830
|
+
operation,
|
|
2831
|
+
type: "media.transform",
|
|
2832
|
+
});
|
|
2833
|
+
}}
|
|
2736
2834
|
preview={
|
|
2737
2835
|
previewMediaAsset
|
|
2738
2836
|
? {
|
|
@@ -2740,6 +2838,7 @@ export function ControlsPanel({
|
|
|
2740
2838
|
alt: previewMediaAsset.fileName,
|
|
2741
2839
|
size: previewMediaAsset.size,
|
|
2742
2840
|
src: previewMediaAsset.dataUrl,
|
|
2841
|
+
transform: previewMediaAsset.transform,
|
|
2743
2842
|
}
|
|
2744
2843
|
: undefined
|
|
2745
2844
|
}
|
|
@@ -2814,7 +2913,7 @@ export function ControlsPanel({
|
|
|
2814
2913
|
: undefined
|
|
2815
2914
|
}
|
|
2816
2915
|
key={id}
|
|
2817
|
-
|
|
2916
|
+
onValueChange={commit}
|
|
2818
2917
|
value={
|
|
2819
2918
|
isRecord(value)
|
|
2820
2919
|
? (value as React.ComponentProps<typeof Palette>["value"])
|
|
@@ -2835,7 +2934,7 @@ export function ControlsPanel({
|
|
|
2835
2934
|
return (
|
|
2836
2935
|
<PanelActions
|
|
2837
2936
|
actions={actions.map((action) => ({
|
|
2838
|
-
icon: action
|
|
2937
|
+
icon: getPanelActionIcon(action),
|
|
2839
2938
|
name: getActionLabel(action),
|
|
2840
2939
|
value: action.value,
|
|
2841
2940
|
variant: getPanelActionButtonVariant(action.variant),
|
|
@@ -2863,6 +2962,7 @@ export function ControlsPanel({
|
|
|
2863
2962
|
key={id}
|
|
2864
2963
|
name={name}
|
|
2865
2964
|
onValueChange={commit}
|
|
2965
|
+
showLabel={control.label !== false}
|
|
2866
2966
|
start={rangeValue.start}
|
|
2867
2967
|
/>
|
|
2868
2968
|
),
|
|
@@ -2925,10 +3025,10 @@ export function ControlsPanel({
|
|
|
2925
3025
|
return (
|
|
2926
3026
|
<Select
|
|
2927
3027
|
key={id}
|
|
2928
|
-
layout={isInInlineLayoutGroup ? "stacked" : undefined}
|
|
2929
3028
|
name={name}
|
|
2930
3029
|
onValueChange={commit}
|
|
2931
3030
|
options={control.options ?? []}
|
|
3031
|
+
showLabel={control.label !== false}
|
|
2932
3032
|
value={asString(value, control.options?.[0]?.value ?? "")}
|
|
2933
3033
|
/>
|
|
2934
3034
|
);
|
|
@@ -3008,6 +3108,7 @@ export function ControlsPanel({
|
|
|
3008
3108
|
key={id}
|
|
3009
3109
|
name={name}
|
|
3010
3110
|
onValueChange={commit}
|
|
3111
|
+
showLabel={control.label !== false}
|
|
3011
3112
|
value={asString(value)}
|
|
3012
3113
|
/>
|
|
3013
3114
|
),
|
|
@@ -3020,6 +3121,7 @@ export function ControlsPanel({
|
|
|
3020
3121
|
|
|
3021
3122
|
case "vector": {
|
|
3022
3123
|
const vectorValue = asVectorValue(value);
|
|
3124
|
+
const padVariant = asVectorPadVariant(control.variant);
|
|
3023
3125
|
|
|
3024
3126
|
return withKeyframeLabelAction({
|
|
3025
3127
|
children: (
|
|
@@ -3028,8 +3130,12 @@ export function ControlsPanel({
|
|
|
3028
3130
|
key={id}
|
|
3029
3131
|
name={name}
|
|
3030
3132
|
onValueChange={commit}
|
|
3133
|
+
padCoordinateMode={asVectorPadCoordinateMode(
|
|
3134
|
+
control.coordinateMode,
|
|
3135
|
+
padVariant,
|
|
3136
|
+
)}
|
|
3031
3137
|
padShape={vectorPadShape}
|
|
3032
|
-
padVariant={
|
|
3138
|
+
padVariant={padVariant}
|
|
3033
3139
|
x={vectorValue.x}
|
|
3034
3140
|
xLabel={control.xLabel}
|
|
3035
3141
|
y={vectorValue.y}
|
|
@@ -48,7 +48,7 @@ describe("Toolcraft template runtime public API", () => {
|
|
|
48
48
|
>().toEqualTypeOf<ControlsPanelKeys>();
|
|
49
49
|
expectTypeOf<
|
|
50
50
|
keyof React.ComponentProps<typeof TimelinePanel>
|
|
51
|
-
>().toEqualTypeOf<RuntimePanelKeys | "defaultExpanded">();
|
|
51
|
+
>().toEqualTypeOf<RuntimePanelKeys | "defaultExpanded" | "variant">();
|
|
52
52
|
expectTypeOf<
|
|
53
53
|
keyof React.ComponentProps<typeof ToolbarPanel>
|
|
54
54
|
>().toEqualTypeOf<RuntimePanelKeys>();
|
|
@@ -64,6 +64,8 @@ describe("settings transfer", () => {
|
|
|
64
64
|
const state = createToolcraftState(schema, {
|
|
65
65
|
values: {
|
|
66
66
|
"generation.prompt": "Exported prompt",
|
|
67
|
+
"panels.timeline.extended": true,
|
|
68
|
+
"panels.timeline.visible": false,
|
|
67
69
|
"style.opacity": 24,
|
|
68
70
|
},
|
|
69
71
|
});
|
|
@@ -82,6 +84,8 @@ describe("settings transfer", () => {
|
|
|
82
84
|
},
|
|
83
85
|
version: 1,
|
|
84
86
|
});
|
|
87
|
+
expect(payload.values).not.toHaveProperty("panels.timeline.extended");
|
|
88
|
+
expect(payload.values).not.toHaveProperty("panels.timeline.visible");
|
|
85
89
|
expect(parseToolcraftSettingsPayload(schema, payload)).toEqual(payload);
|
|
86
90
|
expect(
|
|
87
91
|
parseToolcraftSettingsPayload(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResolvedToolcraftAppSchema } from "../schema/types";
|
|
2
|
+
import { isToolcraftTimelinePanelRuntimeTarget } from "../schema/runtime-targets";
|
|
2
3
|
import type {
|
|
3
4
|
ToolcraftCommand,
|
|
4
5
|
ToolcraftState,
|
|
@@ -50,7 +51,11 @@ function getKnownValueTargets(schema: ResolvedToolcraftAppSchema): Set<string> {
|
|
|
50
51
|
|
|
51
52
|
for (const section of schema.panels.controls?.sections ?? []) {
|
|
52
53
|
for (const control of Object.values(section.controls)) {
|
|
53
|
-
if (
|
|
54
|
+
if (
|
|
55
|
+
control.type !== "panelActions" &&
|
|
56
|
+
control.type !== "settingsTransfer" &&
|
|
57
|
+
!isToolcraftTimelinePanelRuntimeTarget(control.target)
|
|
58
|
+
) {
|
|
54
59
|
targets.add(control.target);
|
|
55
60
|
}
|
|
56
61
|
}
|
|
@@ -97,6 +97,20 @@ describe("TimelinePanel", () => {
|
|
|
97
97
|
expect(screen.queryByText("Add your first keyframe from the properties panel.")).toBeNull();
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
it("renders compact presentation as playback-only transport", () => {
|
|
101
|
+
renderTimelinePanel({ variant: "compact" });
|
|
102
|
+
|
|
103
|
+
expect(screen.getByRole("button", { name: "Pause playback" })).toBeTruthy();
|
|
104
|
+
expect(screen.queryByRole("button", { name: "Disable loop" })).toBeNull();
|
|
105
|
+
expect(screen.queryByText("Dur:")).toBeNull();
|
|
106
|
+
expect(screen.queryByText("Duration:")).toBeNull();
|
|
107
|
+
expect(screen.queryByRole("slider", { name: "Playback position" })).toBeNull();
|
|
108
|
+
|
|
109
|
+
fireEvent.click(screen.getByRole("button", { name: "Pause playback" }));
|
|
110
|
+
|
|
111
|
+
expect(screen.getByTestId("timeline-playing").textContent).toBe("false");
|
|
112
|
+
});
|
|
113
|
+
|
|
100
114
|
it("renders timeline state and dispatches playback commands", () => {
|
|
101
115
|
renderTimelinePanel();
|
|
102
116
|
|
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
ToolcraftTimelineKeyframeEasing,
|
|
32
32
|
ToolcraftTimelineKeyframeGroup,
|
|
33
33
|
} from '../state/types';
|
|
34
|
+
import { getToolcraftTimelineLoopTime } from '../state/timeline-loop';
|
|
34
35
|
import { isTimelineReadyForPlayback } from '../state/timeline-readiness';
|
|
35
36
|
import { PanelContainer } from './panel-host';
|
|
36
37
|
import type { PanelPlacement, PanelStateChange } from './panel-host-types';
|
|
@@ -43,6 +44,7 @@ type TimelinePanelProps = {
|
|
|
43
44
|
onPanelStateChange?: PanelStateChange;
|
|
44
45
|
panelPlacement?: PanelPlacement;
|
|
45
46
|
panelState?: ToolcraftPanelState;
|
|
47
|
+
variant?: 'compact' | 'extended';
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
function cn(...classNames: Array<string | false | null | undefined>): string {
|
|
@@ -50,6 +52,7 @@ function cn(...classNames: Array<string | false | null | undefined>): string {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
const timelinePanelCollapsedSize = { height: 36 } as const;
|
|
55
|
+
const timelinePanelCompactWidthPx = 36;
|
|
53
56
|
const timelinePanelCollapsedWidthPx = 256;
|
|
54
57
|
const timelinePanelExpandedWidthPx = 688;
|
|
55
58
|
const timelinePanelMinResponsiveWidthPx = 320;
|
|
@@ -2336,12 +2339,15 @@ function useTimelineClock({
|
|
|
2336
2339
|
setCurrentTimeSeconds((currentValue) => {
|
|
2337
2340
|
const nextValue = currentValue + elapsedSeconds;
|
|
2338
2341
|
|
|
2339
|
-
if (nextValue
|
|
2342
|
+
if (nextValue < durationSeconds) {
|
|
2340
2343
|
return nextValue;
|
|
2341
2344
|
}
|
|
2342
2345
|
|
|
2343
2346
|
if (isLooping) {
|
|
2344
|
-
return
|
|
2347
|
+
return getToolcraftTimelineLoopTime({
|
|
2348
|
+
currentTimeSeconds: nextValue,
|
|
2349
|
+
durationSeconds,
|
|
2350
|
+
});
|
|
2345
2351
|
}
|
|
2346
2352
|
|
|
2347
2353
|
setIsPlaying(false);
|
|
@@ -2608,6 +2614,7 @@ function TimelinePanelHeader({
|
|
|
2608
2614
|
onToggleLoop,
|
|
2609
2615
|
onTogglePlayback,
|
|
2610
2616
|
stripRef,
|
|
2617
|
+
variant,
|
|
2611
2618
|
}: {
|
|
2612
2619
|
canExpand: boolean;
|
|
2613
2620
|
currentTimeSeconds: number;
|
|
@@ -2626,7 +2633,31 @@ function TimelinePanelHeader({
|
|
|
2626
2633
|
onToggleLoop: () => void;
|
|
2627
2634
|
onTogglePlayback: () => void;
|
|
2628
2635
|
stripRef: React.RefObject<HTMLDivElement | null>;
|
|
2636
|
+
variant: 'compact' | 'extended';
|
|
2629
2637
|
}): React.JSX.Element {
|
|
2638
|
+
if (variant === 'compact') {
|
|
2639
|
+
return (
|
|
2640
|
+
<div
|
|
2641
|
+
className="relative flex h-full min-w-0 shrink-0 items-center justify-center"
|
|
2642
|
+
data-slot="timeline-panel-header"
|
|
2643
|
+
data-timeline-panel-variant="compact"
|
|
2644
|
+
>
|
|
2645
|
+
<div
|
|
2646
|
+
className="relative z-10 inline-flex shrink-0 items-center"
|
|
2647
|
+
data-slot="timeline-transport-controls"
|
|
2648
|
+
>
|
|
2649
|
+
<TimelineIconButton
|
|
2650
|
+
disabled={!playbackReady}
|
|
2651
|
+
label={isPlaying ? 'Pause playback' : 'Play playback'}
|
|
2652
|
+
onClick={onTogglePlayback}
|
|
2653
|
+
>
|
|
2654
|
+
{isPlaying ? <Pause /> : <Play />}
|
|
2655
|
+
</TimelineIconButton>
|
|
2656
|
+
</div>
|
|
2657
|
+
</div>
|
|
2658
|
+
);
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2630
2661
|
return (
|
|
2631
2662
|
<div
|
|
2632
2663
|
className={cn(
|
|
@@ -2705,6 +2736,7 @@ export function TimelinePanel({
|
|
|
2705
2736
|
onPanelStateChange,
|
|
2706
2737
|
panelPlacement,
|
|
2707
2738
|
panelState,
|
|
2739
|
+
variant = 'extended',
|
|
2708
2740
|
}: TimelinePanelProps): React.JSX.Element | null {
|
|
2709
2741
|
const { dispatch, state } = useToolcraft();
|
|
2710
2742
|
|
|
@@ -2727,7 +2759,8 @@ export function TimelinePanel({
|
|
|
2727
2759
|
const [defaultExpandedPending, setDefaultExpandedPending] = useState(defaultExpanded);
|
|
2728
2760
|
const [isHoverPaused, setIsHoverPaused] = useState(false);
|
|
2729
2761
|
const displayedIsPlaying = playbackReady && isPlaying;
|
|
2730
|
-
const
|
|
2762
|
+
const isCompact = variant === 'compact';
|
|
2763
|
+
const isExpanded = !isCompact && keyframesEnabled && (expanded || defaultExpandedPending);
|
|
2731
2764
|
const expandedPanelSize = getTimelinePanelExpandedSize(keyframeGroups);
|
|
2732
2765
|
const previousIsExpandedRef = useRef(isExpanded);
|
|
2733
2766
|
const timelineRef = useRef(state.timeline);
|
|
@@ -2911,7 +2944,9 @@ export function TimelinePanel({
|
|
|
2911
2944
|
const { panelRef: timelineSurfaceRef, responsiveLayout } = useTimelinePanelResponsiveLayout(
|
|
2912
2945
|
isExpanded && !shouldConstrainToContainer,
|
|
2913
2946
|
);
|
|
2914
|
-
const unconstrainedTimelinePanelWidth =
|
|
2947
|
+
const unconstrainedTimelinePanelWidth = isCompact
|
|
2948
|
+
? timelinePanelCompactWidthPx
|
|
2949
|
+
: isExpanded
|
|
2915
2950
|
? expandedPanelSize.width
|
|
2916
2951
|
: timelinePanelCollapsedWidthPx;
|
|
2917
2952
|
const timelinePanelWidth =
|
|
@@ -2947,6 +2982,7 @@ export function TimelinePanel({
|
|
|
2947
2982
|
data-playback-ready={playbackReady ? 'true' : 'false'}
|
|
2948
2983
|
data-scrubbing={scrubber.isScrubbing ? 'true' : 'false'}
|
|
2949
2984
|
data-slot="timeline-panel"
|
|
2985
|
+
data-timeline-panel-variant={variant}
|
|
2950
2986
|
initial={false}
|
|
2951
2987
|
ref={timelineSurfaceRef}
|
|
2952
2988
|
style={timelinePanelLayoutStyle}
|
|
@@ -2956,7 +2992,7 @@ export function TimelinePanel({
|
|
|
2956
2992
|
className={cn(
|
|
2957
2993
|
'group/timeline-panel-surface relative flex h-full w-full flex-col rounded-t-lg rounded-b-lg',
|
|
2958
2994
|
isExpanded ? 'overflow-hidden' : 'overflow-visible p-1',
|
|
2959
|
-
!isExpanded && !keyframesEnabled && 'pr-3',
|
|
2995
|
+
!isCompact && !isExpanded && !keyframesEnabled && 'pr-3',
|
|
2960
2996
|
)}
|
|
2961
2997
|
data-panel-id="timeline"
|
|
2962
2998
|
onPointerEnter={() => setIsHoverPaused(true)}
|
|
@@ -2970,13 +3006,13 @@ export function TimelinePanel({
|
|
|
2970
3006
|
setIsHoverPaused(false);
|
|
2971
3007
|
}}
|
|
2972
3008
|
>
|
|
2973
|
-
{isExpanded ?
|
|
3009
|
+
{!isCompact && !isExpanded ? (
|
|
2974
3010
|
<TimelinePanelMask
|
|
2975
3011
|
currentTimeSeconds={currentTimeSeconds}
|
|
2976
3012
|
durationSeconds={durationSeconds}
|
|
2977
3013
|
isHandleVisible={isHoverPaused || scrubber.isScrubbing}
|
|
2978
3014
|
/>
|
|
2979
|
-
)}
|
|
3015
|
+
) : null}
|
|
2980
3016
|
<TimelinePanelHeader
|
|
2981
3017
|
canExpand={keyframesEnabled}
|
|
2982
3018
|
currentTimeSeconds={currentTimeSeconds}
|
|
@@ -3001,6 +3037,7 @@ export function TimelinePanel({
|
|
|
3001
3037
|
dispatch({ type: 'timeline.togglePlayback' });
|
|
3002
3038
|
}}
|
|
3003
3039
|
stripRef={scrubber.stripRef}
|
|
3040
|
+
variant={variant}
|
|
3004
3041
|
/>
|
|
3005
3042
|
{isExpanded && keyframesEnabled ? (
|
|
3006
3043
|
<TimelineExpandedContent
|
|
@@ -188,6 +188,10 @@ describe("assembled Toolcraft template runtime", () => {
|
|
|
188
188
|
renderAssembledEditor();
|
|
189
189
|
|
|
190
190
|
expect(screen.getByText("Layers")).toBeTruthy();
|
|
191
|
+
const timelineField = screen.getByText("Timeline").closest('[role="group"]');
|
|
192
|
+
const timelineSwitch = timelineField?.querySelector<HTMLElement>('[role="switch"]');
|
|
193
|
+
|
|
194
|
+
fireEvent.click(timelineSwitch as HTMLElement);
|
|
191
195
|
fireEvent.click(
|
|
192
196
|
screen.getByRole("button", { name: "Expand timeline panel" }),
|
|
193
197
|
);
|
|
@@ -266,7 +270,8 @@ describe("assembled Toolcraft template runtime", () => {
|
|
|
266
270
|
).toBe("320px");
|
|
267
271
|
expect(screen.getByText("Master Controls")).toBeTruthy();
|
|
268
272
|
expect(screen.getByText("Layers")).toBeTruthy();
|
|
269
|
-
expect(screen.
|
|
273
|
+
expect(screen.getByRole("button", { name: "Pause playback" })).toBeTruthy();
|
|
274
|
+
expect(screen.queryByText("Dur:")).toBeNull();
|
|
270
275
|
expect(screen.queryByText("Duration:")).toBeNull();
|
|
271
276
|
expect(toolbar?.textContent).toContain("100%");
|
|
272
277
|
expect(screen.getByRole("button", { name: "Undo" })).toBeDisabled();
|
|
@@ -339,6 +344,9 @@ describe("assembled Toolcraft template runtime", () => {
|
|
|
339
344
|
});
|
|
340
345
|
expect(screen.getByRole("button", { name: "Undo" })).not.toBeDisabled();
|
|
341
346
|
|
|
347
|
+
const timelineField = screen.getByText("Timeline").closest('[role="group"]');
|
|
348
|
+
const timelineSwitch = timelineField?.querySelector<HTMLElement>('[role="switch"]');
|
|
349
|
+
fireEvent.click(timelineSwitch as HTMLElement);
|
|
342
350
|
fireEvent.click(
|
|
343
351
|
screen.getByRole("button", { name: "Expand timeline panel" }),
|
|
344
352
|
);
|