"use client";
import * as React from "react";
import type {
ResolvedToolcraftControlSectionSchema,
ResolvedToolcraftControlSchema,
ToolcraftControlSchema,
} from "../../../schema/types";
import {
getToolcraftCanvasSizeTargetDimension,
isToolcraftRuntimeOwnedTarget,
} from "../../../schema/runtime-targets";
import { getToolcraftControlRendererKind } from "../renderers/controls-panel-renderer-registry";
export type ControlEntry = [string, ResolvedToolcraftControlSchema];
export type ControlRenderGroup =
| { entries: readonly ControlEntry[]; kind: "colorGroup" }
| { entry: ControlEntry; kind: "control" };
export type RenderedControlRenderGroup = {
ids: readonly string[];
node: React.ReactNode;
};
export function isControlsPanelRenderableControl(
control: ToolcraftControlSchema,
): boolean {
return getToolcraftControlRendererKind(control.type) !== "canvas-handle";
}
export function getControlsPanelRenderableEntries(
entries: readonly ControlEntry[],
): ControlEntry[] {
return entries.filter(([, control]) =>
isControlsPanelRenderableControl(control),
);
}
const hiddenDiscreteMarkerCount = 2;
const sectionedCompoundControlTypes = new Set([
"channelMixer",
"collectionActions",
"sourceCollection",
"fontPicker",
"gradient",
"palette",
]);
export function shouldRenderCompoundControlSectionDivider(
control: ToolcraftControlSchema,
): boolean {
if (control.type === "curves") {
return control.variant !== "single";
}
return sectionedCompoundControlTypes.has(control.type);
}
export function withCompoundControlSectionDivider({
children,
control,
}: {
children: React.ReactNode;
control: ToolcraftControlSchema;
}): React.ReactNode {
if (!shouldRenderCompoundControlSectionDivider(control)) {
return children;
}
return (
{children}
);
}
export function getControlName(
id: string,
label: boolean | string | undefined,
): string {
if (typeof label === "string") {
return label;
}
return id;
}
export function getControlsRecord(
entries: readonly ControlEntry[],
): Record {
return Object.fromEntries(entries);
}
export function getControlRenderGroups(
entries: readonly ControlEntry[],
sectionHasOnlyColorFields: boolean,
): ControlRenderGroup[] {
const groups: ControlRenderGroup[] = [];
let index = 0;
while (index < entries.length) {
const entry = entries[index];
if (!entry) {
index += 1;
continue;
}
const colorBankKey = getPlainColorBankKey(
entry[1],
sectionHasOnlyColorFields,
);
if (!colorBankKey) {
groups.push({ entry, kind: "control" });
index += 1;
continue;
}
const colorEntries: ControlEntry[] = [];
while (
entries[index] &&
getPlainColorBankKey(
entries[index][1],
sectionHasOnlyColorFields,
) === colorBankKey
) {
const colorEntry = entries[index];
if (colorEntry) {
colorEntries.push(colorEntry);
}
index += 1;
}
for (
let colorIndex = 0;
colorIndex < colorEntries.length;
colorIndex += 2
) {
const firstEntry = colorEntries[colorIndex];
const secondEntry = colorEntries[colorIndex + 1];
if (firstEntry && secondEntry) {
groups.push({ entries: [firstEntry, secondEntry], kind: "colorGroup" });
} else if (firstEntry) {
groups.push({ entry: firstEntry, kind: "control" });
}
}
}
return groups;
}
function getPlainColorBankKey(
control: ToolcraftControlSchema,
sectionHasOnlyColorFields: boolean,
): string | null {
if (control.type !== "color") {
return null;
}
if (sectionHasOnlyColorFields) {
return "color-only-section";
}
const semanticGroup = control.semanticGroup?.trim();
return semanticGroup ? `semantic:${semanticGroup}` : null;
}
export function getControlRenderGroupIds(
group: ControlRenderGroup,
): readonly string[] {
return group.kind === "colorGroup"
? group.entries.map(([id]) => id)
: [group.entry[0]];
}
export function countControlsByType(
sections: readonly ResolvedToolcraftControlSectionSchema[],
type: string,
): number {
return sections.reduce(
(count, section) =>
count +
Object.values(section.controls).filter((control) => control.type === type)
.length,
0,
);
}
export function shouldCommitTextControlOnBlur(
control: ToolcraftControlSchema,
): boolean {
return (
control.commitMode === "setting" ||
Boolean(getToolcraftCanvasSizeTargetDimension(control.target))
);
}
function isColorFieldControl(
control: ToolcraftControlSchema | undefined,
): boolean {
return control?.type === "color" || control?.type === "colorOpacity";
}
export function isColorOnlySection(entries: readonly ControlEntry[]): boolean {
return (
entries.length > 0 &&
entries.every(([, control]) => isColorFieldControl(control))
);
}
export function getRenderedControlsSectionTitle(
section: ResolvedToolcraftControlSectionSchema,
): ResolvedToolcraftControlSectionSchema["title"] {
return isStickyFooterActionSection(section) ? undefined : section.title;
}
export function isRuntimeSetupSection({
entries,
section,
}: {
entries: readonly ControlEntry[];
section: ResolvedToolcraftControlSectionSchema;
}): boolean {
return (
section.title === "Setup" &&
entries.some(([, control]) => isToolcraftRuntimeOwnedTarget(control.target))
);
}
function isStickyFooterActionSection(
section: ResolvedToolcraftControlSectionSchema,
): boolean {
return (
section.actionGroup !== undefined &&
Object.values(section.controls).some(
(control) => control.type === "panelActions",
)
);
}
export function shouldShowColorFieldLabel({
control,
sectionHasOnlyColorFields,
}: {
control: ToolcraftControlSchema;
sectionHasOnlyColorFields: boolean;
}): boolean {
return control.label !== false && !sectionHasOnlyColorFields;
}
export function getControlMarkerCount(
control: ToolcraftControlSchema,
markerLimit?: number,
): number | undefined {
const markerCount = control.markerCount;
if (
markerLimit &&
control.variant === "discrete" &&
typeof markerCount === "number" &&
markerCount > markerLimit
) {
return hiddenDiscreteMarkerCount;
}
return markerCount;
}