@pixel-point/toolcraft 0.0.7 → 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 +185 -23
- package/templates/runtime/contracts/component-contracts.ts +91 -36
- package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
- package/templates/runtime/contracts/decision-contracts.ts +4 -4
- package/templates/runtime/export/export.test.ts +31 -0
- package/templates/runtime/export/export.ts +41 -0
- package/templates/runtime/react/canvas-shell.test.tsx +77 -1
- package/templates/runtime/react/canvas-shell.tsx +178 -23
- package/templates/runtime/react/control-conditions.ts +166 -0
- package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
- package/templates/runtime/react/controls-panel.test.tsx +774 -17
- package/templates/runtime/react/controls-panel.tsx +155 -8
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +46 -1
- package/templates/runtime/schema/define-toolcraft.ts +29 -3
- package/templates/runtime/schema/types.ts +7 -0
- package/templates/runtime/state/reducer.test.ts +304 -0
- package/templates/runtime/state/reducer.ts +148 -9
- package/templates/runtime/state/types.ts +10 -2
- package/templates/runtime/testing/performance.test.ts +1424 -56
- package/templates/runtime/testing/performance.ts +710 -43
- package/templates/starter/AGENTS.md +10 -9
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +16 -8
- package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -7
- package/templates/starter/docs/toolcraft/component-rules.md +46 -16
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +54 -6
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +48 -19
- package/templates/starter/docs/toolcraft/workflow.md +2 -2
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +5 -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 +1288 -105
- package/templates/starter/src/app/starter-acceptance.ts +740 -24
- package/templates/starter/src/app/starter-performance.test.ts +66 -5
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +10 -4
- 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/file-drop/file-drop-control.tsx +340 -44
- package/templates/ui/components/controls/file-drop/index.ts +1 -1
- package/templates/ui/components/controls/index.ts +3 -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 +82 -6
|
@@ -147,9 +147,11 @@ export type ToolcraftCustomControlCoverage =
|
|
|
147
147
|
const builtInToolcraftControlTypeValues = [
|
|
148
148
|
"actions",
|
|
149
149
|
"anchorGrid",
|
|
150
|
+
"aspectRatio",
|
|
150
151
|
"channelMixer",
|
|
151
152
|
"checkbox",
|
|
152
153
|
"code",
|
|
154
|
+
"collectionActions",
|
|
153
155
|
"color",
|
|
154
156
|
"colorOpacity",
|
|
155
157
|
"curves",
|
|
@@ -171,6 +173,8 @@ const builtInToolcraftControlTypeValues = [
|
|
|
171
173
|
] as const;
|
|
172
174
|
const settingsTransferOptOutReasonPattern =
|
|
173
175
|
/\b(ephemeral|temporary|one-off|not portable|session-only)\b/i;
|
|
176
|
+
const insufficientFixedCanvasSizingReasonPattern =
|
|
177
|
+
/\b(no|without|missing|lacks?|does\s+not\s+(?:have|expose)|did\s+not\s+(?:have|expose)|has\s+no|had\s+no)\b[^.]{0,80}\b(?:size|dimension|canvas|output)\b[^.]{0,80}\b(?:editor|controls?|settings?|picker|input|ui)\b|\breference\s+app\b[^.]{0,120}\b(?:no|without|missing|lacks?|does\s+not\s+(?:have|expose)|did\s+not\s+(?:have|expose)|has\s+no|had\s+no)\b[^.]{0,80}\b(?:size|dimension|canvas|output)\b/i;
|
|
174
178
|
|
|
175
179
|
export type ToolcraftBuiltInControlType =
|
|
176
180
|
(typeof builtInToolcraftControlTypeValues)[number];
|
|
@@ -263,6 +267,15 @@ export type ToolcraftControlOrderItem = {
|
|
|
263
267
|
type: string;
|
|
264
268
|
};
|
|
265
269
|
|
|
270
|
+
export type ToolcraftControlSectionInventoryEntry = {
|
|
271
|
+
entity?: string;
|
|
272
|
+
groupingReason: string;
|
|
273
|
+
splitReason?: string;
|
|
274
|
+
targets: readonly string[];
|
|
275
|
+
title: string;
|
|
276
|
+
workflowStage?: string;
|
|
277
|
+
};
|
|
278
|
+
|
|
266
279
|
export const starterTransferMode: ToolcraftTransferMode = {
|
|
267
280
|
animationIntent: { mode: "none" },
|
|
268
281
|
mode: "new-toolcraft-app",
|
|
@@ -276,10 +289,16 @@ export const starterProductReadiness: ToolcraftProductReadiness = {
|
|
|
276
289
|
|
|
277
290
|
export const starterAcceptance: readonly ToolcraftComponentAcceptance[] = [];
|
|
278
291
|
|
|
292
|
+
export const starterControlSectionInventory: readonly ToolcraftControlSectionInventoryEntry[] = [];
|
|
293
|
+
|
|
279
294
|
function getActionValue(action: ToolcraftActionSchema | string): string {
|
|
280
295
|
return typeof action === "string" ? action : action.value;
|
|
281
296
|
}
|
|
282
297
|
|
|
298
|
+
function getActionLabelText(action: ToolcraftActionSchema | string): string {
|
|
299
|
+
return typeof action === "string" ? action : (action.label ?? action.value);
|
|
300
|
+
}
|
|
301
|
+
|
|
283
302
|
function getActionSearchText(action: ToolcraftActionSchema | string): string {
|
|
284
303
|
return typeof action === "string" ? action : `${action.label} ${action.value} ${action.command ?? ""}`;
|
|
285
304
|
}
|
|
@@ -385,6 +404,34 @@ function getAcceptanceEvidenceText(entry: ToolcraftComponentAcceptance): string
|
|
|
385
404
|
].join(" ");
|
|
386
405
|
}
|
|
387
406
|
|
|
407
|
+
function getFileDropLifecycleCoverageErrors(
|
|
408
|
+
label: string,
|
|
409
|
+
entry: ToolcraftComponentAcceptance,
|
|
410
|
+
): string[] {
|
|
411
|
+
const errors: string[] = [];
|
|
412
|
+
const evidenceText = getAcceptanceEvidenceText(entry);
|
|
413
|
+
|
|
414
|
+
if (entry.evidence !== "media-lifecycle") {
|
|
415
|
+
errors.push(
|
|
416
|
+
`${label} fileDrop acceptance evidence must be "media-lifecycle" so upload, clear, and reset behavior cannot be replaced by generic product-output coverage.`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
!/\b(upload|import|drop|drag|browse|choose|select file|source image)\b/i.test(
|
|
422
|
+
evidenceText,
|
|
423
|
+
) ||
|
|
424
|
+
!/\b(clear|remove|delete|trash)\b/i.test(evidenceText) ||
|
|
425
|
+
!/\b(reset|reset controls|section reset|global reset)\b/i.test(evidenceText)
|
|
426
|
+
) {
|
|
427
|
+
errors.push(
|
|
428
|
+
`${label} fileDrop acceptance must prove upload/import, clear/remove, and section or global reset remove source media.`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return errors;
|
|
433
|
+
}
|
|
434
|
+
|
|
388
435
|
const conditionOperatorLabels = [
|
|
389
436
|
"equals",
|
|
390
437
|
"notEquals",
|
|
@@ -520,9 +567,109 @@ function isCustomToolcraftControl(control: ToolcraftControlSchema): boolean {
|
|
|
520
567
|
return !builtInToolcraftControlTypes.has(control.type);
|
|
521
568
|
}
|
|
522
569
|
|
|
570
|
+
const collectionEntityCustomControlRe =
|
|
571
|
+
/\b(collection|repeatable|list|lists|item|items|entry|entries|row|rows|asset|assets|object|objects|color|colors|swatch|swatches|glyph|glyphs|symbol|symbols|point|points|stop|stops|variant|variants|rule|rules|mask|masks|shape|shapes|layer|layers|media|image|images|file|files)\b/i;
|
|
572
|
+
const collectionOperationCustomControlRe =
|
|
573
|
+
/\b(add|adding|delete|deleting|remove|removing|reorder|reordering|order|ordering|sort|sorting|select|selecting|selected|selection|duplicate|duplicating|upload|import|clear|clearing)\b/i;
|
|
574
|
+
|
|
575
|
+
const actionLikeCustomControlRe =
|
|
576
|
+
/\b(add|adding|delete|deleting|remove|removing|duplicate|duplicating|sort|sorting|normalize|normalizing|clear|clearing|reset|shuffle|randomize|randomizing)\b/i;
|
|
577
|
+
|
|
578
|
+
const chromeOnlyCustomControlReasonRe =
|
|
579
|
+
/\b(icon|icons|visual|style|styling|layout|spacing|chrome|button|buttons|compact|custom look|custom ui)\b/i;
|
|
580
|
+
const productInteractionCustomControlReasonRe =
|
|
581
|
+
/\b(runtime|state|canvas|output|export|upload|import|preview|reorder|ordering|sort|drag|resize|handle|threshold|density|mapping|geometry|nested|multi|multiple|per-item|metadata|hit target|validation|selection)\b/i;
|
|
582
|
+
const collectionValueKeyRe =
|
|
583
|
+
/^(items?|entries|rows|assets|objects|colors?|glyphs?|symbols?|points?|stops?|variants?|rules?|masks?|shapes?|layers?|media|images?|files?)$/i;
|
|
584
|
+
|
|
585
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
586
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function stringifyUnknownForFitCheck(value: unknown): string {
|
|
590
|
+
if (value == null) {
|
|
591
|
+
return "";
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
595
|
+
return String(value);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (Array.isArray(value)) {
|
|
599
|
+
return value.map(stringifyUnknownForFitCheck).join(" ");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (isRecord(value)) {
|
|
603
|
+
return Object.entries(value)
|
|
604
|
+
.flatMap(([key, entryValue]) => [key, stringifyUnknownForFitCheck(entryValue)])
|
|
605
|
+
.join(" ");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return "";
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function arrayLooksLikeCollectionValue(value: readonly unknown[]): boolean {
|
|
612
|
+
return (
|
|
613
|
+
value.length === 0 ||
|
|
614
|
+
value.some((item) => isRecord(item) || Array.isArray(item) || typeof item === "string")
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function hasCollectionValueShape(value: unknown): boolean {
|
|
619
|
+
if (!isRecord(value)) {
|
|
620
|
+
return Array.isArray(value) && arrayLooksLikeCollectionValue(value);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return Object.entries(value).some(([key, entryValue]) => {
|
|
624
|
+
if (Array.isArray(entryValue)) {
|
|
625
|
+
return collectionValueKeyRe.test(key) || arrayLooksLikeCollectionValue(entryValue);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return collectionValueKeyRe.test(key) && isRecord(entryValue);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function getCustomFitCheckSearchText(
|
|
633
|
+
entry: ToolcraftComponentAcceptance,
|
|
634
|
+
control?: ToolcraftControlSchema,
|
|
635
|
+
): string {
|
|
636
|
+
return [
|
|
637
|
+
control?.type,
|
|
638
|
+
control?.target,
|
|
639
|
+
typeof control?.label === "string" ? control.label : undefined,
|
|
640
|
+
stringifyUnknownForFitCheck(control?.defaultValue),
|
|
641
|
+
entry.id,
|
|
642
|
+
entry.target,
|
|
643
|
+
entry.componentType,
|
|
644
|
+
entry.expectedObservable,
|
|
645
|
+
entry.userAction,
|
|
646
|
+
entry.builtInFitCheck?.whyInsufficient,
|
|
647
|
+
entry.builtInFitCheck?.productObservable,
|
|
648
|
+
]
|
|
649
|
+
.filter(Boolean)
|
|
650
|
+
.join(" ");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function isCollectionLikeCustomControl(
|
|
654
|
+
entry: ToolcraftComponentAcceptance,
|
|
655
|
+
control: ToolcraftControlSchema,
|
|
656
|
+
): boolean {
|
|
657
|
+
if (hasCollectionValueShape(control.defaultValue)) {
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const searchText = getCustomFitCheckSearchText(entry, control);
|
|
662
|
+
|
|
663
|
+
return (
|
|
664
|
+
collectionEntityCustomControlRe.test(searchText) &&
|
|
665
|
+
collectionOperationCustomControlRe.test(searchText)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
523
669
|
function getBuiltInFitCheckErrors(
|
|
524
670
|
label: string,
|
|
525
671
|
entry: ToolcraftComponentAcceptance,
|
|
672
|
+
control: ToolcraftControlSchema,
|
|
526
673
|
): string[] {
|
|
527
674
|
const fitCheck = entry.builtInFitCheck;
|
|
528
675
|
|
|
@@ -574,6 +721,32 @@ function getBuiltInFitCheckErrors(
|
|
|
574
721
|
);
|
|
575
722
|
}
|
|
576
723
|
|
|
724
|
+
const searchText = getCustomFitCheckSearchText(entry, control);
|
|
725
|
+
|
|
726
|
+
if (
|
|
727
|
+
isCollectionLikeCustomControl(entry, control) &&
|
|
728
|
+
!checkedBuiltIns.includes("collectionActions")
|
|
729
|
+
) {
|
|
730
|
+
errors.push(
|
|
731
|
+
`${label} builtInFitCheck.checkedBuiltIns must include collectionActions when the custom control owns a growable, removable, selectable, or reorderable runtime item set.`,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (actionLikeCustomControlRe.test(searchText) && !checkedBuiltIns.includes("actions")) {
|
|
736
|
+
errors.push(
|
|
737
|
+
`${label} builtInFitCheck.checkedBuiltIns must include actions when the custom control exposes local command buttons such as add, remove, delete, duplicate, sort, normalize, or clear.`,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (
|
|
742
|
+
chromeOnlyCustomControlReasonRe.test(fitCheck.whyInsufficient) &&
|
|
743
|
+
!productInteractionCustomControlReasonRe.test(fitCheck.whyInsufficient)
|
|
744
|
+
) {
|
|
745
|
+
errors.push(
|
|
746
|
+
`${label} builtInFitCheck.whyInsufficient cannot justify a custom control only with icons, layout, styling, or custom buttons; name the product interaction or value model that built-ins cannot express.`,
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
577
750
|
return errors;
|
|
578
751
|
}
|
|
579
752
|
|
|
@@ -714,6 +887,68 @@ function getControlLabelText(control: ToolcraftControlSchema): string {
|
|
|
714
887
|
return typeof control.label === "string" ? control.label : "";
|
|
715
888
|
}
|
|
716
889
|
|
|
890
|
+
function hasVisibleControlLabel(control: ToolcraftControlSchema): boolean {
|
|
891
|
+
return typeof control.label === "string" && control.label.trim().length > 0;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const shortSingleLineTextMaxLength = 80;
|
|
895
|
+
const longTextControlEvidencePattern =
|
|
896
|
+
/\b(multiline|multi-line|multi\s+line|long\s+(?:text|content|prompt|instructions?)|paragraph|body\s+(?:copy|text)|rich\s+text|textarea|instructions?|script|code|json|css|shader|template|markdown|csv|xml|yaml|html|svg|list|lines|dataset|structured|source\s+text)\b/i;
|
|
897
|
+
const shortSingleLineTextIntentPattern =
|
|
898
|
+
/\b(button|label|title|name|caption|headline|cta|badge|token|word|phrase|single-line|one-line|short\s+text)\b/i;
|
|
899
|
+
|
|
900
|
+
function getControlDefaultString(control: ToolcraftControlSchema): string | undefined {
|
|
901
|
+
return typeof control.defaultValue === "string" ? control.defaultValue : undefined;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function getTextValueControlIntentText(
|
|
905
|
+
controlId: string,
|
|
906
|
+
control: ToolcraftControlSchema,
|
|
907
|
+
): string {
|
|
908
|
+
return [
|
|
909
|
+
controlId,
|
|
910
|
+
control.target,
|
|
911
|
+
getControlLabelText(control),
|
|
912
|
+
control.description ?? "",
|
|
913
|
+
]
|
|
914
|
+
.join(" ")
|
|
915
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function getCodeTextareaControlError({
|
|
919
|
+
control,
|
|
920
|
+
controlId,
|
|
921
|
+
label,
|
|
922
|
+
}: {
|
|
923
|
+
control: ToolcraftControlSchema;
|
|
924
|
+
controlId: string;
|
|
925
|
+
label: string;
|
|
926
|
+
}): string | undefined {
|
|
927
|
+
if (control.type !== "code") {
|
|
928
|
+
return undefined;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const defaultValue = getControlDefaultString(control);
|
|
932
|
+
const intentText = getTextValueControlIntentText(controlId, control);
|
|
933
|
+
const hasLongTextEvidence =
|
|
934
|
+
longTextControlEvidencePattern.test(intentText) ||
|
|
935
|
+
(defaultValue !== undefined &&
|
|
936
|
+
(defaultValue.length > shortSingleLineTextMaxLength ||
|
|
937
|
+
/[\r\n]/.test(defaultValue)));
|
|
938
|
+
const looksLikeShortSingleLineText =
|
|
939
|
+
shortSingleLineTextIntentPattern.test(intentText) ||
|
|
940
|
+
(defaultValue !== undefined &&
|
|
941
|
+
defaultValue.trim().length > 0 &&
|
|
942
|
+
defaultValue.trim().length <= shortSingleLineTextMaxLength &&
|
|
943
|
+
!/[\r\n]/.test(defaultValue));
|
|
944
|
+
|
|
945
|
+
if (!looksLikeShortSingleLineText || hasLongTextEvidence) {
|
|
946
|
+
return undefined;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return `${label} uses CodeTextarea for short single-line text. Use type "text" for button labels, canvas labels, titles, captions, names, badges, tokens, and other short one-line content. Reserve type "code" for long, multiline, or structured values, and document that reason in description when the default is short.`;
|
|
950
|
+
}
|
|
951
|
+
|
|
717
952
|
const singleCurveSemanticPattern =
|
|
718
953
|
/\b(acceleration|accel|bend|easing|ease|response|depth|mask|opacity|alpha|motion|velocity|threshold|falloff|remap|remapping)\b|speed\s+profile|mapping\s+curve|curve\s+mapping/i;
|
|
719
954
|
const rgbCurveSemanticPattern =
|
|
@@ -772,6 +1007,33 @@ function getToggleControlLabelError(
|
|
|
772
1007
|
return undefined;
|
|
773
1008
|
}
|
|
774
1009
|
|
|
1010
|
+
function getSingleActionsControlLabelError(
|
|
1011
|
+
control: ToolcraftControlSchema,
|
|
1012
|
+
): string | undefined {
|
|
1013
|
+
if (control.type !== "actions") {
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const actions = getControlActions(control);
|
|
1018
|
+
|
|
1019
|
+
if (actions.length !== 1) {
|
|
1020
|
+
return undefined;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const label = getControlLabelText(control).trim();
|
|
1024
|
+
const actionLabel = getActionLabelText(actions[0] as ToolcraftActionSchema | string).trim();
|
|
1025
|
+
|
|
1026
|
+
if (
|
|
1027
|
+
label &&
|
|
1028
|
+
actionLabel &&
|
|
1029
|
+
normalizeToolcraftSemanticText(label) === normalizeToolcraftSemanticText(actionLabel)
|
|
1030
|
+
) {
|
|
1031
|
+
return `single Actions control label "${label}" duplicates its only button label "${actionLabel}". Keep the button as the command and use a short context label such as "Ink wash", "Palette action", or "Current layer".`;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return undefined;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
775
1037
|
const maxInlineSwitchLabelLength = 16;
|
|
776
1038
|
const maxInlineSwitchLabelWordCount = 2;
|
|
777
1039
|
|
|
@@ -1160,7 +1422,7 @@ function isModeSelectorControl(
|
|
|
1160
1422
|
return false;
|
|
1161
1423
|
}
|
|
1162
1424
|
|
|
1163
|
-
return /mode|type|filter|
|
|
1425
|
+
return /mode|type|filter|style|preset|variant/i.test(
|
|
1164
1426
|
`${controlId} ${control.target} ${getControlLabelText(control)}`,
|
|
1165
1427
|
);
|
|
1166
1428
|
}
|
|
@@ -1286,7 +1548,7 @@ const maxHardControlsPerSection = 10;
|
|
|
1286
1548
|
|
|
1287
1549
|
const controlSemanticClusterPatterns: ReadonlyArray<readonly [string, RegExp]> = [
|
|
1288
1550
|
["input", /\b(upload|source|prompt|content|text|phrase|copy|message|file|media|image)\b/i],
|
|
1289
|
-
["mode", /\b(mode|type|preset|style|variant|
|
|
1551
|
+
["mode", /\b(mode|type|preset|style|variant|filter|layout|format|quality)\b/i],
|
|
1290
1552
|
["motion", /\b(animation|speed|velocity|accel|acceleration|correlation|duration|timing|loop|phase|fps|rate)\b/i],
|
|
1291
1553
|
["geometry", /\b(width|height|size|scale|position|offset|anchor|origin|target|radius|distance|spread|bend|curve|curves|path|shape|grid|gap)\b/i],
|
|
1292
1554
|
["density", /\b(fill|density|amount|count|ratio|word|words|letter|letters|particle|particles|layer|layers|island|islands)\b/i],
|
|
@@ -1524,6 +1786,73 @@ function getToolcraftControlSemanticCluster(
|
|
|
1524
1786
|
return inferToolcraftControlOrderRole(controlId, control);
|
|
1525
1787
|
}
|
|
1526
1788
|
|
|
1789
|
+
function normalizeToolcraftConditionValue(value: unknown): string | null {
|
|
1790
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1791
|
+
const normalized = normalizeToolcraftSemanticText(String(value));
|
|
1792
|
+
return normalized || null;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
function getToolcraftConditionBranchTexts(
|
|
1799
|
+
condition: ToolcraftControlConditionSchema,
|
|
1800
|
+
gateControl: ToolcraftControlSchema,
|
|
1801
|
+
): string[] {
|
|
1802
|
+
const rawValues =
|
|
1803
|
+
"equals" in condition
|
|
1804
|
+
? [condition.equals]
|
|
1805
|
+
: Array.isArray(condition.oneOf)
|
|
1806
|
+
? [...condition.oneOf]
|
|
1807
|
+
: [];
|
|
1808
|
+
const normalizedValues = rawValues
|
|
1809
|
+
.map(normalizeToolcraftConditionValue)
|
|
1810
|
+
.filter((value): value is string => Boolean(value));
|
|
1811
|
+
const optionLabels = (gateControl.options ?? [])
|
|
1812
|
+
.filter((option) =>
|
|
1813
|
+
normalizedValues.includes(normalizeToolcraftSemanticText(option.value)),
|
|
1814
|
+
)
|
|
1815
|
+
.map((option) => normalizeToolcraftSemanticText(option.label))
|
|
1816
|
+
.filter(Boolean);
|
|
1817
|
+
|
|
1818
|
+
return [...new Set([...normalizedValues, ...optionLabels])];
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function sectionTitleLooksLikeDependencyBranch({
|
|
1822
|
+
condition,
|
|
1823
|
+
gateControl,
|
|
1824
|
+
sectionTitle,
|
|
1825
|
+
}: {
|
|
1826
|
+
condition: ToolcraftControlConditionSchema;
|
|
1827
|
+
gateControl: ToolcraftControlSchema;
|
|
1828
|
+
sectionTitle: string | undefined;
|
|
1829
|
+
}): boolean {
|
|
1830
|
+
const sectionText = normalizeToolcraftSemanticText(sectionTitle);
|
|
1831
|
+
|
|
1832
|
+
if (!sectionText) {
|
|
1833
|
+
return false;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
return getToolcraftConditionBranchTexts(condition, gateControl).some(
|
|
1837
|
+
(branchText) =>
|
|
1838
|
+
branchText.length > 2 &&
|
|
1839
|
+
(sectionText === branchText ||
|
|
1840
|
+
sectionText.includes(branchText) ||
|
|
1841
|
+
branchText.includes(sectionText)),
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function isToolcraftProductSectionControl(control: ToolcraftControlSchema): boolean {
|
|
1846
|
+
return (
|
|
1847
|
+
control.type !== "panelActions" &&
|
|
1848
|
+
control.type !== "settingsTransfer" &&
|
|
1849
|
+
control.target !== "canvas.aspectRatio" &&
|
|
1850
|
+
control.target !== "canvas.renderScale" &&
|
|
1851
|
+
control.target !== "canvas.size.width" &&
|
|
1852
|
+
control.target !== "canvas.size.height"
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1527
1856
|
function getToolcraftGenericControlLabelError({
|
|
1528
1857
|
control,
|
|
1529
1858
|
controlId,
|
|
@@ -1603,13 +1932,23 @@ function getToolcraftControlDescriptionError({
|
|
|
1603
1932
|
}
|
|
1604
1933
|
|
|
1605
1934
|
function isToolcraftColorSectionTitle(sectionTitle: string | undefined): boolean {
|
|
1606
|
-
return /\b(colou?rs?|palette|palettes)\b/i.test(
|
|
1935
|
+
return /\b(colou?rs?|palette|palettes|shades?|accents?)\b/i.test(
|
|
1936
|
+
sectionTitle ?? "",
|
|
1937
|
+
);
|
|
1607
1938
|
}
|
|
1608
1939
|
|
|
1609
1940
|
function isToolcraftSequentialColorLabel(label: string): boolean {
|
|
1610
1941
|
return /^colou?r\s+\d+$/i.test(label.trim());
|
|
1611
1942
|
}
|
|
1612
1943
|
|
|
1944
|
+
function isToolcraftPaletteVariationTarget(target: string): boolean {
|
|
1945
|
+
return (
|
|
1946
|
+
/(?:^|\.)(?:palette|palettes|colou?rs?|shades?|accents?)\b/i.test(
|
|
1947
|
+
target,
|
|
1948
|
+
) || /(?:^|\.)(?:accent|shade|colou?r)\d+\b/i.test(target)
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1613
1952
|
function isToolcraftSimplePaletteDistributionLabel(label: string): boolean {
|
|
1614
1953
|
return /^(spread|mix|distribution)$/i.test(label.trim());
|
|
1615
1954
|
}
|
|
@@ -1648,8 +1987,257 @@ function isToolcraftObviousColorSectionControlDescription({
|
|
|
1648
1987
|
);
|
|
1649
1988
|
}
|
|
1650
1989
|
|
|
1990
|
+
function getToolcraftColorBankLabelErrors({
|
|
1991
|
+
controls,
|
|
1992
|
+
sectionLabel,
|
|
1993
|
+
sectionTitle,
|
|
1994
|
+
}: {
|
|
1995
|
+
controls: readonly [string, ToolcraftControlSchema][];
|
|
1996
|
+
sectionLabel: string;
|
|
1997
|
+
sectionTitle: string | undefined;
|
|
1998
|
+
}): string[] {
|
|
1999
|
+
const colorControls = controls.filter(([, control]) => {
|
|
2000
|
+
if (control.type !== "color" && control.type !== "colorOpacity") {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
return true;
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
if (colorControls.length < 2) {
|
|
2008
|
+
return [];
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
const loosePrefixes = new Set(
|
|
2012
|
+
colorControls
|
|
2013
|
+
.map(([, control]) => getToolcraftLooseTargetPrefix(control.target))
|
|
2014
|
+
.filter((prefix): prefix is string => Boolean(prefix)),
|
|
2015
|
+
);
|
|
2016
|
+
|
|
2017
|
+
if (loosePrefixes.size !== 1) {
|
|
2018
|
+
return [];
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
const sequentialColorControls = colorControls.filter(([, control]) =>
|
|
2022
|
+
isToolcraftSequentialColorLabel(getControlLabelText(control)),
|
|
2023
|
+
);
|
|
2024
|
+
const isPaletteVariationBank =
|
|
2025
|
+
colorControls.every(([, control]) =>
|
|
2026
|
+
isToolcraftPaletteVariationTarget(control.target),
|
|
2027
|
+
) ||
|
|
2028
|
+
(isToolcraftColorSectionTitle(sectionTitle) &&
|
|
2029
|
+
sequentialColorControls.length > 0);
|
|
2030
|
+
|
|
2031
|
+
if (!isPaletteVariationBank) {
|
|
2032
|
+
return [];
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
const visibleColorControls = colorControls.filter(([, control]) =>
|
|
2036
|
+
hasVisibleControlLabel(control),
|
|
2037
|
+
);
|
|
2038
|
+
const errors: string[] = [];
|
|
2039
|
+
|
|
2040
|
+
if (
|
|
2041
|
+
visibleColorControls.length > 0 &&
|
|
2042
|
+
visibleColorControls.length < colorControls.length
|
|
2043
|
+
) {
|
|
2044
|
+
errors.push(
|
|
2045
|
+
`${sectionLabel} mixes labeled and unlabeled color items in one palette variation group. Decide label visibility for the whole group: omit all per-item labels when colors only add variety, or label every item only when each color has a distinct user-facing role.`,
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
for (const [controlId, control] of sequentialColorControls) {
|
|
2050
|
+
const label = getControlLabelText(control).trim();
|
|
2051
|
+
|
|
2052
|
+
errors.push(
|
|
2053
|
+
`${sectionLabel} / ${controlId} uses visible label "${label}" for a palette variation color. When colors only add variety to one shared palette, set label: false or use collectionActions with unlabeled items. Keep visible labels only when each color edits a distinct user-facing entity such as Fill, Stroke, Background, Connector, or Object color.`,
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
return errors;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function getToolcraftSectionInventoryByTitle(
|
|
2061
|
+
sectionInventory: readonly ToolcraftControlSectionInventoryEntry[],
|
|
2062
|
+
): Map<string, ToolcraftControlSectionInventoryEntry> {
|
|
2063
|
+
return new Map(
|
|
2064
|
+
sectionInventory.map((entry) => [entry.title.trim(), entry]),
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
function hasToolcraftInventorySplitEvidence({
|
|
2069
|
+
sectionInventoryByTitle,
|
|
2070
|
+
sections,
|
|
2071
|
+
}: {
|
|
2072
|
+
sectionInventoryByTitle: ReadonlyMap<string, ToolcraftControlSectionInventoryEntry>;
|
|
2073
|
+
sections: ReadonlySet<string>;
|
|
2074
|
+
}): boolean {
|
|
2075
|
+
const entries = [...sections].map((sectionLabel) =>
|
|
2076
|
+
sectionInventoryByTitle.get(sectionLabel),
|
|
2077
|
+
);
|
|
2078
|
+
|
|
2079
|
+
return (
|
|
2080
|
+
entries.length === sections.size &&
|
|
2081
|
+
entries.every(
|
|
2082
|
+
(entry) =>
|
|
2083
|
+
entry &&
|
|
2084
|
+
(entry.workflowStage?.trim().length ?? 0) > 0 &&
|
|
2085
|
+
(entry.splitReason?.trim().length ?? 0) >= 12,
|
|
2086
|
+
)
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function getToolcraftControlSectionInventoryErrors(
|
|
2091
|
+
schema: ResolvedToolcraftAppSchema,
|
|
2092
|
+
sectionInventory: readonly ToolcraftControlSectionInventoryEntry[],
|
|
2093
|
+
): string[] {
|
|
2094
|
+
const errors: string[] = [];
|
|
2095
|
+
|
|
2096
|
+
if (sectionInventory.length === 0) {
|
|
2097
|
+
return errors;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
const schemaSections = (schema.panels.controls?.sections ?? []).flatMap(
|
|
2101
|
+
(section, sectionIndex) => {
|
|
2102
|
+
const targets = Object.values(section.controls)
|
|
2103
|
+
.filter(isToolcraftProductSectionControl)
|
|
2104
|
+
.map((control) => control.target);
|
|
2105
|
+
|
|
2106
|
+
if (targets.length === 0) {
|
|
2107
|
+
return [];
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
return [
|
|
2111
|
+
{
|
|
2112
|
+
label: getToolcraftSectionLabel(section.title, sectionIndex),
|
|
2113
|
+
targets,
|
|
2114
|
+
},
|
|
2115
|
+
];
|
|
2116
|
+
},
|
|
2117
|
+
);
|
|
2118
|
+
const schemaSectionTitles = new Set(schemaSections.map((section) => section.label));
|
|
2119
|
+
const schemaTargetToSection = new Map<string, string>();
|
|
2120
|
+
|
|
2121
|
+
for (const section of schemaSections) {
|
|
2122
|
+
for (const target of section.targets) {
|
|
2123
|
+
schemaTargetToSection.set(target, section.label);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
const inventoryTitleCounts = new Map<string, number>();
|
|
2128
|
+
const inventoryTargetToSection = new Map<string, string>();
|
|
2129
|
+
|
|
2130
|
+
for (const entry of sectionInventory) {
|
|
2131
|
+
const title = entry.title.trim();
|
|
2132
|
+
const entity = entry.entity?.trim() ?? "";
|
|
2133
|
+
const workflowStage = entry.workflowStage?.trim() ?? "";
|
|
2134
|
+
const groupingReason = entry.groupingReason.trim();
|
|
2135
|
+
const splitReason = entry.splitReason?.trim() ?? "";
|
|
2136
|
+
|
|
2137
|
+
if (!title) {
|
|
2138
|
+
errors.push(
|
|
2139
|
+
"Control Section Inventory contains an entry without a section title.",
|
|
2140
|
+
);
|
|
2141
|
+
continue;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
inventoryTitleCounts.set(title, (inventoryTitleCounts.get(title) ?? 0) + 1);
|
|
2145
|
+
|
|
2146
|
+
if (!schemaSectionTitles.has(title)) {
|
|
2147
|
+
errors.push(
|
|
2148
|
+
`Control Section Inventory references "${title}", but no rendered product controls section uses that title.`,
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
if (!entity && !workflowStage) {
|
|
2153
|
+
errors.push(
|
|
2154
|
+
`Control Section Inventory entry "${title}" must declare entity or workflowStage so grouping is based on product meaning, not UI layout.`,
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
if (groupingReason.length < 12) {
|
|
2159
|
+
errors.push(
|
|
2160
|
+
`Control Section Inventory entry "${title}" must include a concrete groupingReason explaining why these controls belong together.`,
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
if (splitReason && splitReason.length < 12) {
|
|
2165
|
+
errors.push(
|
|
2166
|
+
`Control Section Inventory entry "${title}" splitReason is too vague. Explain the product workflow split or omit splitReason.`,
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
if (entry.targets.length === 0) {
|
|
2171
|
+
errors.push(
|
|
2172
|
+
`Control Section Inventory entry "${title}" must list the product control targets rendered in that section.`,
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
for (const target of entry.targets) {
|
|
2177
|
+
const schemaSection = schemaTargetToSection.get(target);
|
|
2178
|
+
|
|
2179
|
+
if (!schemaSection) {
|
|
2180
|
+
errors.push(
|
|
2181
|
+
`Control Section Inventory entry "${title}" lists target "${target}", but that target is not rendered by any product controls section.`,
|
|
2182
|
+
);
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
if (schemaSection !== title) {
|
|
2187
|
+
errors.push(
|
|
2188
|
+
`Control Section Inventory entry "${title}" lists target "${target}", but the schema renders it in "${schemaSection}". Update the schema grouping or the inventory.`,
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
const existingSection = inventoryTargetToSection.get(target);
|
|
2193
|
+
|
|
2194
|
+
if (existingSection && existingSection !== title) {
|
|
2195
|
+
errors.push(
|
|
2196
|
+
`Control Section Inventory lists target "${target}" in both "${existingSection}" and "${title}". Each product control target belongs to exactly one section inventory entry.`,
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
inventoryTargetToSection.set(target, title);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
for (const [title, count] of inventoryTitleCounts) {
|
|
2205
|
+
if (count > 1) {
|
|
2206
|
+
errors.push(
|
|
2207
|
+
`Control Section Inventory repeats section "${title}" ${count} times. Section inventory entries must be unique.`,
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
for (const section of schemaSections) {
|
|
2213
|
+
const entry = sectionInventory.find(
|
|
2214
|
+
(inventoryEntry) => inventoryEntry.title.trim() === section.label,
|
|
2215
|
+
);
|
|
2216
|
+
|
|
2217
|
+
if (!entry) {
|
|
2218
|
+
errors.push(
|
|
2219
|
+
`Control Section Inventory is missing product section "${section.label}". Add its entity/workflow stage, targets, and groupingReason.`,
|
|
2220
|
+
);
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
const inventoryTargets = new Set(entry.targets);
|
|
2225
|
+
|
|
2226
|
+
for (const target of section.targets) {
|
|
2227
|
+
if (!inventoryTargets.has(target)) {
|
|
2228
|
+
errors.push(
|
|
2229
|
+
`Control Section Inventory entry "${section.label}" is missing rendered target "${target}". The inventory must cover every product control in the section.`,
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
return errors;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
1651
2238
|
function getToolcraftControlSectionGroupingErrors(
|
|
1652
2239
|
schema: ResolvedToolcraftAppSchema,
|
|
2240
|
+
sectionInventory: readonly ToolcraftControlSectionInventoryEntry[] = [],
|
|
1653
2241
|
): string[] {
|
|
1654
2242
|
const errors: string[] = [];
|
|
1655
2243
|
const visibleControls: Array<{
|
|
@@ -1657,11 +2245,23 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
1657
2245
|
controlId: string;
|
|
1658
2246
|
loosePrefix: string | null;
|
|
1659
2247
|
sectionLabel: string;
|
|
2248
|
+
sectionTitle: string | undefined;
|
|
1660
2249
|
}> = [];
|
|
1661
2250
|
const strictPrefixSections = new Map<string, Set<string>>();
|
|
1662
2251
|
const loosePrefixSections = new Map<string, Set<string>>();
|
|
1663
2252
|
const colorSectionLoosePrefixes = new Map<string, string>();
|
|
2253
|
+
const sectionInventoryByTitle = getToolcraftSectionInventoryByTitle(sectionInventory);
|
|
1664
2254
|
const sectionTitleCounts = new Map<string, { count: number; label: string }>();
|
|
2255
|
+
const controlsByTarget = new Map<
|
|
2256
|
+
string,
|
|
2257
|
+
{
|
|
2258
|
+
control: ToolcraftControlSchema;
|
|
2259
|
+
controlId: string;
|
|
2260
|
+
loosePrefix: string | null;
|
|
2261
|
+
sectionLabel: string;
|
|
2262
|
+
sectionTitle: string | undefined;
|
|
2263
|
+
}
|
|
2264
|
+
>();
|
|
1665
2265
|
|
|
1666
2266
|
for (const [sectionIndex, section] of (schema.panels.controls?.sections ?? []).entries()) {
|
|
1667
2267
|
const sectionTitle = section.title?.trim();
|
|
@@ -1706,11 +2306,8 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
1706
2306
|
.map(([, control]) => getToolcraftLooseTargetPrefix(control.target))
|
|
1707
2307
|
.filter((prefix): prefix is string => Boolean(prefix)),
|
|
1708
2308
|
);
|
|
1709
|
-
const productControls = controls.filter(
|
|
1710
|
-
(
|
|
1711
|
-
control.type !== "settingsTransfer" &&
|
|
1712
|
-
control.target !== "canvas.size.width" &&
|
|
1713
|
-
control.target !== "canvas.size.height",
|
|
2309
|
+
const productControls = controls.filter(([, control]) =>
|
|
2310
|
+
isToolcraftProductSectionControl(control),
|
|
1714
2311
|
);
|
|
1715
2312
|
const semanticClusters = new Set(
|
|
1716
2313
|
productControls.map(([controlId, control]) =>
|
|
@@ -1737,6 +2334,14 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
1737
2334
|
);
|
|
1738
2335
|
}
|
|
1739
2336
|
|
|
2337
|
+
errors.push(
|
|
2338
|
+
...getToolcraftColorBankLabelErrors({
|
|
2339
|
+
controls,
|
|
2340
|
+
sectionLabel,
|
|
2341
|
+
sectionTitle,
|
|
2342
|
+
}),
|
|
2343
|
+
);
|
|
2344
|
+
|
|
1740
2345
|
for (const [controlId, control] of controls) {
|
|
1741
2346
|
const strictPrefix = getToolcraftStrictTargetPrefix(control.target);
|
|
1742
2347
|
const loosePrefix = getToolcraftLooseTargetPrefix(control.target);
|
|
@@ -1768,6 +2373,14 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
1768
2373
|
controlId,
|
|
1769
2374
|
loosePrefix,
|
|
1770
2375
|
sectionLabel,
|
|
2376
|
+
sectionTitle,
|
|
2377
|
+
});
|
|
2378
|
+
controlsByTarget.set(control.target, {
|
|
2379
|
+
control,
|
|
2380
|
+
controlId,
|
|
2381
|
+
loosePrefix,
|
|
2382
|
+
sectionLabel,
|
|
2383
|
+
sectionTitle,
|
|
1771
2384
|
});
|
|
1772
2385
|
|
|
1773
2386
|
if (strictPrefix) {
|
|
@@ -1801,6 +2414,42 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
1801
2414
|
}
|
|
1802
2415
|
}
|
|
1803
2416
|
|
|
2417
|
+
for (const item of visibleControls) {
|
|
2418
|
+
for (const [conditionName, condition] of [
|
|
2419
|
+
["visibleWhen", item.control.visibleWhen],
|
|
2420
|
+
["disabledWhen", item.control.disabledWhen],
|
|
2421
|
+
] as const) {
|
|
2422
|
+
if (!condition || !item.loosePrefix) {
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
const gateControl = controlsByTarget.get(condition.target);
|
|
2427
|
+
|
|
2428
|
+
if (!gateControl || gateControl.sectionLabel === item.sectionLabel) {
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
const sharesTargetEntity =
|
|
2433
|
+
gateControl.loosePrefix !== null &&
|
|
2434
|
+
gateControl.loosePrefix === item.loosePrefix;
|
|
2435
|
+
const looksLikeBranchSection =
|
|
2436
|
+
conditionName === "visibleWhen" &&
|
|
2437
|
+
sectionTitleLooksLikeDependencyBranch({
|
|
2438
|
+
condition,
|
|
2439
|
+
gateControl: gateControl.control,
|
|
2440
|
+
sectionTitle: item.sectionTitle,
|
|
2441
|
+
});
|
|
2442
|
+
|
|
2443
|
+
if (!sharesTargetEntity && !looksLikeBranchSection) {
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
errors.push(
|
|
2448
|
+
`${item.sectionLabel} / ${item.controlId} is gated by ${conditionName} target "${condition.target}" in ${gateControl.sectionLabel}, but it belongs to the same dependency group. Keep selectors and their dependent controls in one semantic section when they describe one product entity or branch; use visibleWhen/disabledWhen inside that section instead of splitting branch controls into their own section.`,
|
|
2449
|
+
);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
1804
2453
|
const fontPickerControls = visibleControls.filter(
|
|
1805
2454
|
(item) => item.control.type === "fontPicker" && item.loosePrefix,
|
|
1806
2455
|
);
|
|
@@ -1833,9 +2482,12 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
1833
2482
|
}
|
|
1834
2483
|
|
|
1835
2484
|
for (const [prefix, sections] of strictPrefixSections) {
|
|
1836
|
-
if (
|
|
2485
|
+
if (
|
|
2486
|
+
sections.size > 1 &&
|
|
2487
|
+
!hasToolcraftInventorySplitEvidence({ sectionInventoryByTitle, sections })
|
|
2488
|
+
) {
|
|
1837
2489
|
errors.push(
|
|
1838
|
-
`Controls for product entity "${prefix}" are split across sections: ${[...sections].join(", ")}. Keep controls for the same product entity in one semantic section unless the
|
|
2490
|
+
`Controls for product entity "${prefix}" are split across sections: ${[...sections].join(", ")}. Keep controls for the same product entity in one semantic section unless the Control Section Inventory declares workflowStage and splitReason for every split section.`,
|
|
1839
2491
|
);
|
|
1840
2492
|
}
|
|
1841
2493
|
}
|
|
@@ -1904,6 +2556,7 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
1904
2556
|
schema: ResolvedToolcraftAppSchema = starterSchema,
|
|
1905
2557
|
acceptance: readonly ToolcraftComponentAcceptance[] = starterAcceptance,
|
|
1906
2558
|
transferMode: ToolcraftTransferMode = starterTransferMode,
|
|
2559
|
+
sectionInventory: readonly ToolcraftControlSectionInventoryEntry[] = starterControlSectionInventory,
|
|
1907
2560
|
): string[] {
|
|
1908
2561
|
const errors: string[] = [];
|
|
1909
2562
|
const controls = collectToolcraftVisibleControls(schema);
|
|
@@ -1934,7 +2587,8 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
1934
2587
|
]);
|
|
1935
2588
|
|
|
1936
2589
|
errors.push(...getToolcraftControlOrderErrors(schema));
|
|
1937
|
-
errors.push(...getToolcraftControlSectionGroupingErrors(schema));
|
|
2590
|
+
errors.push(...getToolcraftControlSectionGroupingErrors(schema, sectionInventory));
|
|
2591
|
+
errors.push(...getToolcraftControlSectionInventoryErrors(schema, sectionInventory));
|
|
1938
2592
|
|
|
1939
2593
|
for (const [sectionIndex, section] of (schema.panels.controls?.sections ?? []).entries()) {
|
|
1940
2594
|
const sectionLabel = getToolcraftSectionLabel(section.title, sectionIndex);
|
|
@@ -1954,6 +2608,16 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
1954
2608
|
);
|
|
1955
2609
|
}
|
|
1956
2610
|
|
|
2611
|
+
const segmentedIds = layoutGroup.controls.filter(
|
|
2612
|
+
(controlId) => section.controls[controlId]?.type === "segmented",
|
|
2613
|
+
);
|
|
2614
|
+
|
|
2615
|
+
if (segmentedIds.length > 0) {
|
|
2616
|
+
errors.push(
|
|
2617
|
+
`${sectionLabel} layoutGroups inline row "${layoutGroup.controls.join(", ")}" includes segmented control ${segmentedIds.join(", ")}. Segmented is full-width and must not share a two-column or half-width row; use Select when a finite choice must fit beside another control.`,
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
|
|
1957
2621
|
const switchEntries = layoutGroup.controls
|
|
1958
2622
|
.map((controlId) => [controlId, section.controls[controlId]] as const)
|
|
1959
2623
|
.filter(
|
|
@@ -1995,6 +2659,16 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
1995
2659
|
`${sectionLabel} layoutGroups inline row "${layoutGroup.controls.join(", ")}" includes toggle label ${unsafeBooleanLabels.map(([controlId, control]) => `${controlId} "${getInlineSwitchLabelText(controlId, control)}"`).join(", ")} that is too long for a compact toggle-plus-parameter row. Keep the toggle label short, such as "Include" inside Background, or stack the controls.`,
|
|
1996
2660
|
);
|
|
1997
2661
|
}
|
|
2662
|
+
|
|
2663
|
+
const visibleParameterLabels = parameterEntries.filter(
|
|
2664
|
+
([, control]) => control.label !== false,
|
|
2665
|
+
);
|
|
2666
|
+
|
|
2667
|
+
if (visibleParameterLabels.length > 0) {
|
|
2668
|
+
errors.push(
|
|
2669
|
+
`${sectionLabel} layoutGroups inline row "${layoutGroup.controls.join(", ")}" pairs a toggle with parameter labels ${visibleParameterLabels.map(([controlId, control]) => `${controlId} "${getControlLabelText(control)}"`).join(", ")}. In toggle-plus-parameter rows, the non-toggle parameter must use label false; if that label is needed, stack the controls instead.`,
|
|
2670
|
+
);
|
|
2671
|
+
}
|
|
1998
2672
|
}
|
|
1999
2673
|
}
|
|
2000
2674
|
|
|
@@ -2121,7 +2795,7 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2121
2795
|
|
|
2122
2796
|
if (!schemaHasOutputBackgroundToggleControl(controls)) {
|
|
2123
2797
|
errors.push(
|
|
2124
|
-
'Product apps with Export PNG must expose export.includeBackground inside the required "Background" section as a Switch labeled "Include". PNG export must pass that runtime value to createToolcraftPngExportCanvas includeBackground; video export keeps the background.',
|
|
2798
|
+
'Product apps with Export PNG must expose export.includeBackground inside the required "Background" section as a Switch labeled "Include". PNG export must pass that runtime value to createToolcraftPngExportCanvas includeBackground; live preview must use shouldIncludeToolcraftPreviewBackground(state); video export keeps the background.',
|
|
2125
2799
|
);
|
|
2126
2800
|
}
|
|
2127
2801
|
|
|
@@ -2556,6 +3230,12 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2556
3230
|
}
|
|
2557
3231
|
|
|
2558
3232
|
if (schema.canvas.sizing.mode === "fixed-output") {
|
|
3233
|
+
if (schemaHasPngExportPanelAction(schema) || schemaHasVideoExportPanelAction(schema)) {
|
|
3234
|
+
errors.push(
|
|
3235
|
+
'Product/output apps with export actions must use canvas.sizing mode "editable-output" so Aspect ratio, Canvas width, and Canvas height are always available. Put reference, fixed-format, or user-requested dimensions in canvas.size as the initial value instead of hiding size controls with "fixed-output".',
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3238
|
+
|
|
2559
3239
|
const fixedCanvasSizingEntry = acceptance.find(
|
|
2560
3240
|
(entry) =>
|
|
2561
3241
|
entry.kind === "runtime" &&
|
|
@@ -2564,14 +3244,20 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2564
3244
|
|
|
2565
3245
|
if (!fixedCanvasSizingEntry) {
|
|
2566
3246
|
errors.push(
|
|
2567
|
-
'canvas.sizing mode "fixed-output" requires a runtime acceptance entry with canvasSizingCoverage "fixed-output-size" explaining why width and height are intentionally non-editable.
|
|
3247
|
+
'canvas.sizing mode "fixed-output" requires a runtime acceptance entry with canvasSizingCoverage "fixed-output-size" explaining why width and height are intentionally non-editable. Product/output apps must use "editable-output"; user-provided, reference, fixed-format, or base/default sizes belong in canvas.size as editable initial values.',
|
|
2568
3248
|
);
|
|
2569
3249
|
} else {
|
|
2570
3250
|
const evidenceText = getAcceptanceEvidenceText(fixedCanvasSizingEntry);
|
|
2571
3251
|
|
|
2572
3252
|
if (!/(fixed|locked|non-editable|not user-editable|must not edit|reference-defined|product-defined)/i.test(evidenceText)) {
|
|
2573
3253
|
errors.push(
|
|
2574
|
-
`${fixedCanvasSizingEntry.id} canvasSizingCoverage "fixed-output-size" must explain why the
|
|
3254
|
+
`${fixedCanvasSizingEntry.id} canvasSizingCoverage "fixed-output-size" must explain why the output dimensions are intentionally fixed for a non-product/internal fixture, not merely initialized from a default size.`,
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
if (insufficientFixedCanvasSizingReasonPattern.test(evidenceText)) {
|
|
3259
|
+
errors.push(
|
|
3260
|
+
`${fixedCanvasSizingEntry.id} canvasSizingCoverage "fixed-output-size" cannot be justified by the reference or previous app lacking a size editor. Exportable product apps must use editable-output; put reference, product-defined, or fixed-format dimensions in canvas.size as initial values instead of hiding size controls.`,
|
|
2575
3261
|
);
|
|
2576
3262
|
}
|
|
2577
3263
|
|
|
@@ -2691,11 +3377,25 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2691
3377
|
const isCustomControl = isCustomToolcraftControl(control);
|
|
2692
3378
|
const isSelectedLayerTarget = control.target.startsWith("selectedLayer.");
|
|
2693
3379
|
const toggleLabelError = getToggleControlLabelError(control, sectionTitle);
|
|
3380
|
+
const singleActionsLabelError = getSingleActionsControlLabelError(control);
|
|
3381
|
+
const codeTextareaError = getCodeTextareaControlError({
|
|
3382
|
+
control,
|
|
3383
|
+
controlId,
|
|
3384
|
+
label,
|
|
3385
|
+
});
|
|
2694
3386
|
|
|
2695
3387
|
if (toggleLabelError) {
|
|
2696
3388
|
errors.push(`${label} ${toggleLabelError}`);
|
|
2697
3389
|
}
|
|
2698
3390
|
|
|
3391
|
+
if (singleActionsLabelError) {
|
|
3392
|
+
errors.push(`${label} ${singleActionsLabelError}`);
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
if (codeTextareaError) {
|
|
3396
|
+
errors.push(codeTextareaError);
|
|
3397
|
+
}
|
|
3398
|
+
|
|
2699
3399
|
if (
|
|
2700
3400
|
control.type === "rangeSlider" &&
|
|
2701
3401
|
Array.isArray(control.defaultValue) &&
|
|
@@ -2798,6 +3498,10 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2798
3498
|
);
|
|
2799
3499
|
}
|
|
2800
3500
|
|
|
3501
|
+
if (control.type === "fileDrop") {
|
|
3502
|
+
errors.push(...getFileDropLifecycleCoverageErrors(label, entry));
|
|
3503
|
+
}
|
|
3504
|
+
|
|
2801
3505
|
if (
|
|
2802
3506
|
isCustomControl &&
|
|
2803
3507
|
!hasCustomControlCoverage(
|
|
@@ -2811,7 +3515,7 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2811
3515
|
}
|
|
2812
3516
|
|
|
2813
3517
|
if (isCustomControl) {
|
|
2814
|
-
errors.push(...getBuiltInFitCheckErrors(label, entry));
|
|
3518
|
+
errors.push(...getBuiltInFitCheckErrors(label, entry, control));
|
|
2815
3519
|
}
|
|
2816
3520
|
|
|
2817
3521
|
if (
|
|
@@ -2841,17 +3545,23 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2841
3545
|
isOutputBackgroundToggleControl({ control, controlId, sectionTitle })
|
|
2842
3546
|
) {
|
|
2843
3547
|
const evidenceText = getAcceptanceEvidenceText(entry);
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
3548
|
+
const provesPngTransparency =
|
|
3549
|
+
/\b(png|image)\b/i.test(evidenceText) &&
|
|
3550
|
+
/\b(transparent|transparency|alpha)\b/i.test(evidenceText);
|
|
3551
|
+
const provesPreviewTransparency =
|
|
3552
|
+
/\b(preview|canvas)\b/i.test(evidenceText) &&
|
|
3553
|
+
/\b(transparent|transparency|alpha|hide|hides|hidden|without background|no background|background off)\b/i.test(
|
|
2850
3554
|
evidenceText,
|
|
2851
|
-
)
|
|
2852
|
-
|
|
3555
|
+
);
|
|
3556
|
+
const provesVideoBackground =
|
|
3557
|
+
/\bvideo\b/i.test(evidenceText) &&
|
|
3558
|
+
/\b(keep|keeps|preserve|preserves|stay|stays|remain|remains|still|background)\b/i.test(
|
|
3559
|
+
evidenceText,
|
|
3560
|
+
);
|
|
3561
|
+
|
|
3562
|
+
if (!provesPngTransparency || !provesPreviewTransparency || !provesVideoBackground) {
|
|
2853
3563
|
errors.push(
|
|
2854
|
-
`${label} controls
|
|
3564
|
+
`${label} controls background inclusion and acceptance must prove disabling it makes PNG output transparent, hides the live preview product background, and keeps video output with the product background.`,
|
|
2855
3565
|
);
|
|
2856
3566
|
}
|
|
2857
3567
|
}
|
|
@@ -2886,6 +3596,12 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2886
3596
|
if (isSliderLikeControl(control)) {
|
|
2887
3597
|
const expectedMarkerCount = getStepMarkerCount(control);
|
|
2888
3598
|
|
|
3599
|
+
if (control.unit === "x") {
|
|
3600
|
+
errors.push(
|
|
3601
|
+
`${label} uses unit "x", but Toolcraft slider values do not use x suffixes. Omit unit for scale, multiplier, intensity, opacity, strength, depth, and shader amount values unless a real measurement unit applies.`,
|
|
3602
|
+
);
|
|
3603
|
+
}
|
|
3604
|
+
|
|
2889
3605
|
if (
|
|
2890
3606
|
control.variant === "discrete" &&
|
|
2891
3607
|
expectedMarkerCount &&
|