@pixel-point/toolcraft 0.0.3 → 0.0.6
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/scripts/prepare-pack.mjs +5 -0
- package/src/generate.mjs +13 -0
- package/src/generate.test.mjs +6 -0
- package/templates/runtime/contracts/component-contracts.test.ts +86 -8
- package/templates/runtime/contracts/component-contracts.ts +36 -8
- package/templates/runtime/contracts/decision-contracts.ts +2 -2
- package/templates/runtime/export/export.test.ts +65 -0
- package/templates/runtime/export/export.ts +54 -1
- package/templates/runtime/react/canvas-shell.test.tsx +7 -7
- package/templates/runtime/react/controls-panel.test.tsx +323 -6
- package/templates/runtime/react/controls-panel.tsx +349 -24
- package/templates/runtime/react/settings-transfer.test.ts +6 -0
- package/templates/runtime/react/settings-transfer.ts +28 -2
- package/templates/runtime/react/timeline-panel.test.tsx +69 -0
- package/templates/runtime/react/timeline-panel.tsx +98 -10
- package/templates/runtime/react/toolbar-panel.test.tsx +6 -6
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +2 -2
- package/templates/runtime/schema/canvas-aspect-ratio-presets.ts +50 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +122 -2
- package/templates/runtime/schema/define-toolcraft.ts +197 -6
- package/templates/runtime/schema/keyframe-capability.test.ts +7 -0
- package/templates/runtime/schema/keyframe-capability.ts +2 -2
- package/templates/runtime/schema/runtime-targets.ts +6 -0
- package/templates/runtime/schema/types.ts +23 -1
- package/templates/runtime/state/canvas-zoom.ts +1 -1
- package/templates/runtime/state/create-template-state.test.ts +9 -3
- package/templates/runtime/state/reducer.test.ts +135 -2
- package/templates/runtime/state/reducer.ts +236 -12
- package/templates/runtime/state/types.ts +1 -0
- package/templates/starter/AGENTS.md +6 -4
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +4 -2
- package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -4
- package/templates/starter/docs/toolcraft/component-rules.md +24 -5
- package/templates/starter/docs/toolcraft/performance.md +5 -0
- package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +53 -9
- package/templates/starter/gitignore +36 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +678 -21
- package/templates/starter/src/app/starter-acceptance.ts +357 -4
- package/templates/ui/components/control-layout/index.tsx +4 -4
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +101 -18
- package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1 -1
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +4 -1
- package/templates/ui/components/controls/slider/slider-value.ts +48 -5
- package/templates/ui/components/primitives/editable-slider-value-label.tsx +6 -1
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
type ColorOpacityValue,
|
|
43
43
|
type CurveInterpolation,
|
|
44
44
|
type FontPickerValue,
|
|
45
|
+
type FileDropPreview,
|
|
45
46
|
type GradientStop,
|
|
46
47
|
type GradientType,
|
|
47
48
|
type ImagePickerItem,
|
|
@@ -57,6 +58,10 @@ import type {
|
|
|
57
58
|
ToolcraftControlSchema,
|
|
58
59
|
ResolvedToolcraftAppSchema,
|
|
59
60
|
} from "../schema/types";
|
|
61
|
+
import {
|
|
62
|
+
getToolcraftCanvasAspectRatioPreset,
|
|
63
|
+
toolcraftCanvasAspectRatioPresets,
|
|
64
|
+
} from "../schema/canvas-aspect-ratio-presets";
|
|
60
65
|
import { getToolcraftControlKeyframeCapability } from "../schema/keyframe-capability";
|
|
61
66
|
import { getToolcraftCanvasSizeTargetDimension } from "../schema/runtime-targets";
|
|
62
67
|
import type {
|
|
@@ -112,9 +117,22 @@ type FooterActionProgressEntry = {
|
|
|
112
117
|
id: number;
|
|
113
118
|
progress: number | null;
|
|
114
119
|
};
|
|
120
|
+
type CanvasAspectRatioValue = {
|
|
121
|
+
height: number;
|
|
122
|
+
mode: "custom" | "preset";
|
|
123
|
+
value: string;
|
|
124
|
+
width: number;
|
|
125
|
+
};
|
|
115
126
|
|
|
116
127
|
const hiddenDiscreteMarkerCount = 2;
|
|
117
128
|
const controlsPanelSectionCollapseStorageVersion = 1;
|
|
129
|
+
const canvasAspectRatioOptions = [
|
|
130
|
+
...toolcraftCanvasAspectRatioPresets.map((preset) => ({
|
|
131
|
+
label: preset.value,
|
|
132
|
+
value: preset.value,
|
|
133
|
+
})),
|
|
134
|
+
{ label: "Custom...", value: "custom" },
|
|
135
|
+
] as const;
|
|
118
136
|
|
|
119
137
|
const sectionedCompoundControlTypes = new Set([
|
|
120
138
|
"channelMixer",
|
|
@@ -257,6 +275,86 @@ function asString(value: unknown, fallback = ""): string {
|
|
|
257
275
|
return typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
|
|
258
276
|
}
|
|
259
277
|
|
|
278
|
+
function parseCanvasAspectRatioOption(value: string): CanvasAspectRatioValue | null {
|
|
279
|
+
const preset = getToolcraftCanvasAspectRatioPreset(value);
|
|
280
|
+
|
|
281
|
+
if (preset) {
|
|
282
|
+
return {
|
|
283
|
+
height: preset.ratioHeight,
|
|
284
|
+
mode: "preset",
|
|
285
|
+
value: preset.value,
|
|
286
|
+
width: preset.ratioWidth,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const match = /^\s*(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)\s*$/u.exec(value);
|
|
291
|
+
|
|
292
|
+
if (!match) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const width = Number.parseFloat(match[1] ?? "");
|
|
297
|
+
const height = Number.parseFloat(match[2] ?? "");
|
|
298
|
+
|
|
299
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
height: Math.round(height),
|
|
305
|
+
mode: "custom",
|
|
306
|
+
value: `${Math.round(width)}:${Math.round(height)}`,
|
|
307
|
+
width: Math.round(width),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function asCanvasAspectRatioValue(
|
|
312
|
+
value: unknown,
|
|
313
|
+
fallback: unknown,
|
|
314
|
+
): CanvasAspectRatioValue {
|
|
315
|
+
if (isRecord(value)) {
|
|
316
|
+
const width = asNumber(value.width, NaN);
|
|
317
|
+
const height = asNumber(value.height, NaN);
|
|
318
|
+
const mode = value.mode === "preset" ? "preset" : "custom";
|
|
319
|
+
|
|
320
|
+
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
|
321
|
+
return {
|
|
322
|
+
height: Math.round(height),
|
|
323
|
+
mode,
|
|
324
|
+
value:
|
|
325
|
+
typeof value.value === "string"
|
|
326
|
+
? value.value
|
|
327
|
+
: `${Math.round(width)}:${Math.round(height)}`,
|
|
328
|
+
width: Math.round(width),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (typeof value === "string") {
|
|
334
|
+
const parsed = parseCanvasAspectRatioOption(value);
|
|
335
|
+
|
|
336
|
+
if (parsed) {
|
|
337
|
+
return parsed;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (fallback !== value) {
|
|
342
|
+
return asCanvasAspectRatioValue(fallback, {
|
|
343
|
+
height: 1,
|
|
344
|
+
mode: "preset",
|
|
345
|
+
value: "1:1",
|
|
346
|
+
width: 1,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
height: 1,
|
|
352
|
+
mode: "preset",
|
|
353
|
+
value: "1:1",
|
|
354
|
+
width: 1,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
260
358
|
function asBoolean(value: unknown, fallback = false): boolean {
|
|
261
359
|
return typeof value === "boolean" ? value : fallback;
|
|
262
360
|
}
|
|
@@ -330,6 +428,8 @@ function formatControlValueLabel(
|
|
|
330
428
|
}
|
|
331
429
|
|
|
332
430
|
switch (control.type) {
|
|
431
|
+
case "aspectRatio":
|
|
432
|
+
return asCanvasAspectRatioValue(value, control.defaultValue).value;
|
|
333
433
|
case "checkbox":
|
|
334
434
|
case "switch":
|
|
335
435
|
return asBoolean(value) ? "On" : "Off";
|
|
@@ -505,6 +605,111 @@ function asFontPickerValue(value: unknown): FontPickerValue {
|
|
|
505
605
|
};
|
|
506
606
|
}
|
|
507
607
|
|
|
608
|
+
function CanvasAspectRatioControl({
|
|
609
|
+
defaultValue,
|
|
610
|
+
name,
|
|
611
|
+
onValueChange,
|
|
612
|
+
value,
|
|
613
|
+
}: {
|
|
614
|
+
defaultValue: unknown;
|
|
615
|
+
name: string;
|
|
616
|
+
onValueChange?: (
|
|
617
|
+
value: CanvasAspectRatioValue,
|
|
618
|
+
meta?: ControlChangeMeta,
|
|
619
|
+
) => void;
|
|
620
|
+
value: unknown;
|
|
621
|
+
}): React.JSX.Element {
|
|
622
|
+
const ratio = asCanvasAspectRatioValue(value, defaultValue);
|
|
623
|
+
const selectedValue = ratio.mode === "custom" ? "custom" : ratio.value;
|
|
624
|
+
|
|
625
|
+
function commitRatio(
|
|
626
|
+
nextRatio: CanvasAspectRatioValue,
|
|
627
|
+
meta?: ControlChangeMeta,
|
|
628
|
+
): void {
|
|
629
|
+
onValueChange?.(nextRatio, meta);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function updatePreset(nextValue: string): void {
|
|
633
|
+
if (nextValue === "custom") {
|
|
634
|
+
commitRatio({
|
|
635
|
+
height: ratio.height,
|
|
636
|
+
mode: "custom",
|
|
637
|
+
value: `${ratio.width}:${ratio.height}`,
|
|
638
|
+
width: ratio.width,
|
|
639
|
+
});
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const nextRatio = parseCanvasAspectRatioOption(nextValue);
|
|
644
|
+
|
|
645
|
+
if (nextRatio) {
|
|
646
|
+
commitRatio(nextRatio);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function updateCustomDimension(
|
|
651
|
+
dimension: "height" | "width",
|
|
652
|
+
nextValue: string,
|
|
653
|
+
meta?: ControlChangeMeta,
|
|
654
|
+
): void {
|
|
655
|
+
const numberValue = Number.parseFloat(nextValue);
|
|
656
|
+
|
|
657
|
+
if (!Number.isFinite(numberValue) || numberValue <= 0) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const width =
|
|
662
|
+
dimension === "width" ? Math.max(1, Math.round(numberValue)) : ratio.width;
|
|
663
|
+
const height =
|
|
664
|
+
dimension === "height" ? Math.max(1, Math.round(numberValue)) : ratio.height;
|
|
665
|
+
|
|
666
|
+
commitRatio(
|
|
667
|
+
{
|
|
668
|
+
height,
|
|
669
|
+
mode: "custom",
|
|
670
|
+
value: `${width}:${height}`,
|
|
671
|
+
width,
|
|
672
|
+
},
|
|
673
|
+
meta,
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return (
|
|
678
|
+
<div className="min-w-0 space-y-2" data-slot="canvas-aspect-ratio-control">
|
|
679
|
+
<Select
|
|
680
|
+
layout="stacked"
|
|
681
|
+
name={name}
|
|
682
|
+
onValueChange={updatePreset}
|
|
683
|
+
options={canvasAspectRatioOptions}
|
|
684
|
+
value={selectedValue}
|
|
685
|
+
/>
|
|
686
|
+
{ratio.mode === "custom" ? (
|
|
687
|
+
<TextInput
|
|
688
|
+
inputs={[
|
|
689
|
+
{
|
|
690
|
+
commitOnBlur: true,
|
|
691
|
+
defaultValue: String(ratio.width),
|
|
692
|
+
name: "Width",
|
|
693
|
+
onValueChange: (nextValue, meta) =>
|
|
694
|
+
updateCustomDimension("width", nextValue, meta),
|
|
695
|
+
value: String(ratio.width),
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
commitOnBlur: true,
|
|
699
|
+
defaultValue: String(ratio.height),
|
|
700
|
+
name: "Height",
|
|
701
|
+
onValueChange: (nextValue, meta) =>
|
|
702
|
+
updateCustomDimension("height", nextValue, meta),
|
|
703
|
+
value: String(ratio.height),
|
|
704
|
+
},
|
|
705
|
+
]}
|
|
706
|
+
inputsPerRow={2}
|
|
707
|
+
/>
|
|
708
|
+
) : null}
|
|
709
|
+
</div>
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
508
713
|
function asActionSchemas(
|
|
509
714
|
actions: readonly (ToolcraftActionSchema | string)[] | undefined,
|
|
510
715
|
): readonly ToolcraftActionSchema[] {
|
|
@@ -690,6 +895,7 @@ function isRuntimeSetupSection(section: ToolcraftControlSectionSchema): boolean
|
|
|
690
895
|
return Object.values(section.controls).some(
|
|
691
896
|
(control) =>
|
|
692
897
|
control.type === "settingsTransfer" ||
|
|
898
|
+
control.target === "canvas.aspectRatio" ||
|
|
693
899
|
control.target === "canvas.size.width" ||
|
|
694
900
|
control.target === "canvas.size.height",
|
|
695
901
|
);
|
|
@@ -1486,12 +1692,94 @@ export function ControlsPanel({
|
|
|
1486
1692
|
);
|
|
1487
1693
|
}
|
|
1488
1694
|
|
|
1489
|
-
function
|
|
1490
|
-
|
|
1491
|
-
|
|
1695
|
+
function normalizeControlHelpContext(value: string): string {
|
|
1696
|
+
return value
|
|
1697
|
+
.toLowerCase()
|
|
1698
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
1699
|
+
.trim();
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function isColorSectionTitle(sectionTitle: string | undefined): boolean {
|
|
1703
|
+
return /\b(colou?rs?|palette|palettes)\b/i.test(sectionTitle ?? "");
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function isSequentialColorLabel(label: string): boolean {
|
|
1707
|
+
return /^colou?r\s+\d+$/i.test(label.trim());
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function isSimplePaletteDistributionLabel(label: string): boolean {
|
|
1711
|
+
return /^(spread|mix|distribution)$/i.test(label.trim());
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function isGenericControlHelpDescription(description: string): boolean {
|
|
1715
|
+
return /^(adjusts?|changes?|chooses?|controls?|defines?|selects?|sets?|updates?)\b/i.test(
|
|
1716
|
+
description.trim(),
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function shouldSuppressObviousControlHelp({
|
|
1721
|
+
control,
|
|
1722
|
+
description,
|
|
1723
|
+
label,
|
|
1724
|
+
sectionTitle,
|
|
1725
|
+
}: {
|
|
1726
|
+
control: ToolcraftControlSchema;
|
|
1727
|
+
description: string;
|
|
1728
|
+
label: string;
|
|
1729
|
+
sectionTitle: string | undefined;
|
|
1730
|
+
}): boolean {
|
|
1731
|
+
const isColorControl = control.type === "color" || control.type === "colorOpacity";
|
|
1732
|
+
|
|
1733
|
+
if (isColorSectionTitle(sectionTitle)) {
|
|
1734
|
+
if (isColorControl && isSequentialColorLabel(label)) {
|
|
1735
|
+
return true;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
if (
|
|
1739
|
+
isSimplePaletteDistributionLabel(label) &&
|
|
1740
|
+
isGenericControlHelpDescription(description)
|
|
1741
|
+
) {
|
|
1742
|
+
return true;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
const normalizedDescription = normalizeControlHelpContext(description);
|
|
1747
|
+
const normalizedLabel = normalizeControlHelpContext(label);
|
|
1748
|
+
|
|
1749
|
+
return (
|
|
1750
|
+
Boolean(normalizedLabel) &&
|
|
1751
|
+
isGenericControlHelpDescription(description) &&
|
|
1752
|
+
normalizedDescription === normalizedLabel
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function getControlHelpText({
|
|
1757
|
+
control,
|
|
1758
|
+
label,
|
|
1759
|
+
sectionTitle,
|
|
1760
|
+
}: {
|
|
1761
|
+
control: ToolcraftControlSchema;
|
|
1762
|
+
label: string;
|
|
1763
|
+
sectionTitle: string | undefined;
|
|
1764
|
+
}): string | null {
|
|
1492
1765
|
const description = control.description?.trim();
|
|
1493
1766
|
|
|
1494
|
-
|
|
1767
|
+
if (!description) {
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
if (
|
|
1772
|
+
shouldSuppressObviousControlHelp({
|
|
1773
|
+
control,
|
|
1774
|
+
description,
|
|
1775
|
+
label,
|
|
1776
|
+
sectionTitle,
|
|
1777
|
+
})
|
|
1778
|
+
) {
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
return description;
|
|
1495
1783
|
}
|
|
1496
1784
|
|
|
1497
1785
|
function withControlLabelHelp({
|
|
@@ -1499,13 +1787,15 @@ export function ControlsPanel({
|
|
|
1499
1787
|
control,
|
|
1500
1788
|
label,
|
|
1501
1789
|
providerKey,
|
|
1790
|
+
sectionTitle,
|
|
1502
1791
|
}: {
|
|
1503
1792
|
children: React.ReactNode;
|
|
1504
1793
|
control: ToolcraftControlSchema;
|
|
1505
1794
|
label: string;
|
|
1506
1795
|
providerKey: string;
|
|
1796
|
+
sectionTitle: string | undefined;
|
|
1507
1797
|
}): React.ReactNode {
|
|
1508
|
-
const help = getControlHelpText(control);
|
|
1798
|
+
const help = getControlHelpText({ control, label, sectionTitle });
|
|
1509
1799
|
|
|
1510
1800
|
if (!help) {
|
|
1511
1801
|
return children;
|
|
@@ -1765,6 +2055,17 @@ export function ControlsPanel({
|
|
|
1765
2055
|
);
|
|
1766
2056
|
}
|
|
1767
2057
|
|
|
2058
|
+
case "aspectRatio":
|
|
2059
|
+
return (
|
|
2060
|
+
<CanvasAspectRatioControl
|
|
2061
|
+
defaultValue={control.defaultValue}
|
|
2062
|
+
key={id}
|
|
2063
|
+
name={name}
|
|
2064
|
+
onValueChange={commit}
|
|
2065
|
+
value={value}
|
|
2066
|
+
/>
|
|
2067
|
+
);
|
|
2068
|
+
|
|
1768
2069
|
case "anchorGrid":
|
|
1769
2070
|
return withKeyframeLabelAction({
|
|
1770
2071
|
children: (
|
|
@@ -1913,13 +2214,38 @@ export function ControlsPanel({
|
|
|
1913
2214
|
}
|
|
1914
2215
|
|
|
1915
2216
|
case "fileDrop": {
|
|
1916
|
-
const
|
|
1917
|
-
|
|
1918
|
-
|
|
2217
|
+
const previewMediaAssets = state.schema.panels.layers ? [] : state.mediaAssets;
|
|
2218
|
+
const previewMediaAsset = previewMediaAssets[0];
|
|
2219
|
+
const previews = previewMediaAssets.map((asset): FileDropPreview => ({
|
|
2220
|
+
alt: asset.fileName,
|
|
2221
|
+
id: asset.id,
|
|
2222
|
+
size: asset.size,
|
|
2223
|
+
src: asset.dataUrl,
|
|
2224
|
+
}));
|
|
2225
|
+
const importFile = (file: File, replaceExisting: boolean): void => {
|
|
2226
|
+
void readImportedImageFile(file, state.canvas.size).then((importedImage) => {
|
|
2227
|
+
if (!importedImage) {
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
dispatchCommand({
|
|
2232
|
+
asset: {
|
|
2233
|
+
dataUrl: importedImage.dataUrl,
|
|
2234
|
+
fileName: file.name,
|
|
2235
|
+
mimeType: file.type || "image/*",
|
|
2236
|
+
position: { x: 0, y: 0 },
|
|
2237
|
+
size: importedImage.size,
|
|
2238
|
+
},
|
|
2239
|
+
replaceExisting,
|
|
2240
|
+
type: "media.import",
|
|
2241
|
+
});
|
|
2242
|
+
});
|
|
2243
|
+
};
|
|
1919
2244
|
|
|
1920
2245
|
return (
|
|
1921
2246
|
<FileDrop
|
|
1922
2247
|
accept={control.accept ?? "PNG, JPEG, GIF, SVG, WebP"}
|
|
2248
|
+
multiple={control.multiple}
|
|
1923
2249
|
key={id}
|
|
1924
2250
|
onClear={
|
|
1925
2251
|
previewMediaAsset
|
|
@@ -1931,33 +2257,31 @@ export function ControlsPanel({
|
|
|
1931
2257
|
}
|
|
1932
2258
|
: undefined
|
|
1933
2259
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
size: importedImage.size,
|
|
1947
|
-
},
|
|
1948
|
-
type: "media.import",
|
|
1949
|
-
});
|
|
2260
|
+
onFilesSelect={(files) => {
|
|
2261
|
+
files.forEach((file) => importFile(file, false));
|
|
2262
|
+
}}
|
|
2263
|
+
onFileSelect={(file) => importFile(file, true)}
|
|
2264
|
+
onPreviewRemove={(item) => {
|
|
2265
|
+
if (!item.id) {
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
dispatchCommand({
|
|
2270
|
+
mediaId: item.id,
|
|
2271
|
+
type: "media.delete",
|
|
1950
2272
|
});
|
|
1951
2273
|
}}
|
|
1952
2274
|
preview={
|
|
1953
2275
|
previewMediaAsset
|
|
1954
2276
|
? {
|
|
2277
|
+
id: previewMediaAsset.id,
|
|
1955
2278
|
alt: previewMediaAsset.fileName,
|
|
1956
2279
|
size: previewMediaAsset.size,
|
|
1957
2280
|
src: previewMediaAsset.dataUrl,
|
|
1958
2281
|
}
|
|
1959
2282
|
: undefined
|
|
1960
2283
|
}
|
|
2284
|
+
previews={previews}
|
|
1961
2285
|
/>
|
|
1962
2286
|
);
|
|
1963
2287
|
}
|
|
@@ -2298,6 +2622,7 @@ export function ControlsPanel({
|
|
|
2298
2622
|
control,
|
|
2299
2623
|
label: name,
|
|
2300
2624
|
providerKey: id,
|
|
2625
|
+
sectionTitle: section.title,
|
|
2301
2626
|
}),
|
|
2302
2627
|
control,
|
|
2303
2628
|
}),
|
|
@@ -130,6 +130,12 @@ describe("settings transfer", () => {
|
|
|
130
130
|
|
|
131
131
|
expect(state.values["generation.prompt"]).toBe("Imported prompt");
|
|
132
132
|
expect(state.values["unknown.target"]).toBeUndefined();
|
|
133
|
+
expect(state.values["canvas.aspectRatio"]).toEqual({
|
|
134
|
+
height: 9,
|
|
135
|
+
mode: "custom",
|
|
136
|
+
value: "16:9",
|
|
137
|
+
width: 16,
|
|
138
|
+
});
|
|
133
139
|
expect(state.canvas.size).toEqual({ height: 900, unit: "px", width: 1600 });
|
|
134
140
|
expect(state.timeline).toMatchObject({
|
|
135
141
|
currentTimeSeconds: 3,
|
|
@@ -123,9 +123,33 @@ export function parseToolcraftSettingsPayload(
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
function applyCanvasSize(
|
|
126
|
-
dispatch:
|
|
126
|
+
{ dispatch, state }: ImportContext,
|
|
127
127
|
size: ToolcraftSettingsTransferPayload["canvas"]["size"],
|
|
128
|
+
options: { deriveAspectRatio: boolean },
|
|
128
129
|
): void {
|
|
130
|
+
const importableTargets = getKnownValueTargets(state.schema);
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
options.deriveAspectRatio &&
|
|
134
|
+
importableTargets.has("canvas.aspectRatio") &&
|
|
135
|
+
isFinitePositiveNumber(size.width) &&
|
|
136
|
+
isFinitePositiveNumber(size.height)
|
|
137
|
+
) {
|
|
138
|
+
dispatch({
|
|
139
|
+
history: "merge",
|
|
140
|
+
historyGroup: settingsTransferImportHistoryGroup,
|
|
141
|
+
label: "Import settings",
|
|
142
|
+
target: "canvas.aspectRatio",
|
|
143
|
+
type: "controls.setValue",
|
|
144
|
+
value: {
|
|
145
|
+
height: size.height,
|
|
146
|
+
mode: "custom",
|
|
147
|
+
value: `${size.width}:${size.height}`,
|
|
148
|
+
width: size.width,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
if (isFinitePositiveNumber(size.width)) {
|
|
130
154
|
dispatch({
|
|
131
155
|
history: "merge",
|
|
@@ -211,7 +235,9 @@ export function applyToolcraftSettingsPayload(
|
|
|
211
235
|
});
|
|
212
236
|
}
|
|
213
237
|
|
|
214
|
-
applyCanvasSize(context
|
|
238
|
+
applyCanvasSize(context, payload.canvas.size, {
|
|
239
|
+
deriveAspectRatio: !Object.hasOwn(payload.values, "canvas.aspectRatio"),
|
|
240
|
+
});
|
|
215
241
|
applyTimeline(context, payload.timeline);
|
|
216
242
|
}
|
|
217
243
|
|
|
@@ -114,6 +114,75 @@ describe("TimelinePanel", () => {
|
|
|
114
114
|
expect(screen.getByTestId("timeline-looping").textContent).toBe("false");
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
it("keeps timeline playhead handles easy to grab without changing visual size", () => {
|
|
118
|
+
const { container } = renderTimelinePanel();
|
|
119
|
+
|
|
120
|
+
const compactHandle = container.querySelector<HTMLElement>(
|
|
121
|
+
'[data-slot="timeline-playback-handle"]',
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(compactHandle?.className).toContain("size-2");
|
|
125
|
+
expect(compactHandle?.className).toContain("before:inset-[-4px]");
|
|
126
|
+
|
|
127
|
+
fireEvent.click(screen.getByRole("button", { name: "Expand timeline panel" }));
|
|
128
|
+
|
|
129
|
+
const expandedHitArea = container.querySelector<HTMLElement>(
|
|
130
|
+
'[data-slot="timeline-expanded-playhead-hit-area"]',
|
|
131
|
+
);
|
|
132
|
+
const expandedHandle = container.querySelector<HTMLElement>(
|
|
133
|
+
'[data-slot="timeline-expanded-playhead-handle"]',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(expandedHandle?.className).toContain("size-[9px]");
|
|
137
|
+
expect(expandedHitArea?.getAttribute("style")).toContain("width: 15px;");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("edits timeline duration from the highlighted duration value in collapsed and expanded states", () => {
|
|
141
|
+
const { container } = renderTimelinePanel();
|
|
142
|
+
|
|
143
|
+
const durationDisplay = container.querySelector<HTMLElement>(
|
|
144
|
+
'[data-slot="timeline-duration-display"]',
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(durationDisplay?.className).toContain("!cursor-text");
|
|
148
|
+
expect(durationDisplay?.className).toContain("group-hover/timeline-panel-header:bg");
|
|
149
|
+
expect(durationDisplay?.className).toContain("var(--foreground)_8%");
|
|
150
|
+
expect(durationDisplay?.className).toContain("px-1");
|
|
151
|
+
expect(container.querySelector('[data-slot="timeline-duration-edit-trigger"]')).toBeNull();
|
|
152
|
+
expect(screen.queryByRole("textbox", { name: "timeline duration" })).toBeNull();
|
|
153
|
+
|
|
154
|
+
fireEvent.click(screen.getByRole("button", { name: "Edit timeline duration" }));
|
|
155
|
+
|
|
156
|
+
const editor = screen.getByRole("textbox", { name: "timeline duration" });
|
|
157
|
+
|
|
158
|
+
expect(editor.className).toContain("var(--foreground)_8%");
|
|
159
|
+
expect(editor.className).toContain("px-1");
|
|
160
|
+
expect(editor.className).toContain("!cursor-text");
|
|
161
|
+
expect(screen.queryByRole("button", { name: "Edit timeline duration" })).toBeNull();
|
|
162
|
+
|
|
163
|
+
editor.textContent = "12s";
|
|
164
|
+
fireEvent.keyDown(editor, { key: "Enter" });
|
|
165
|
+
|
|
166
|
+
expect(screen.getByText("12s")).toBeTruthy();
|
|
167
|
+
|
|
168
|
+
fireEvent.click(screen.getByRole("button", { name: "Expand timeline panel" }));
|
|
169
|
+
|
|
170
|
+
const expandedDurationDisplay = container.querySelector<HTMLElement>(
|
|
171
|
+
'[data-slot="timeline-duration-display"]',
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(expandedDurationDisplay?.className).toContain("!cursor-text");
|
|
175
|
+
expect(expandedDurationDisplay?.className).toContain("group-hover/timeline-panel-header:bg");
|
|
176
|
+
expect(expandedDurationDisplay?.className).toContain("var(--foreground)_8%");
|
|
177
|
+
|
|
178
|
+
fireEvent.click(screen.getByRole("button", { name: "Edit timeline duration" }));
|
|
179
|
+
|
|
180
|
+
const expandedEditor = screen.getByRole("textbox", { name: "timeline duration" });
|
|
181
|
+
|
|
182
|
+
expect(expandedEditor.className).toContain("var(--foreground)_8%");
|
|
183
|
+
expect(expandedEditor.className).toContain("!cursor-text");
|
|
184
|
+
});
|
|
185
|
+
|
|
117
186
|
it("replays from the beginning when play is pressed at the non-looping end", () => {
|
|
118
187
|
renderTimelinePanel(
|
|
119
188
|
{},
|