fix: update field spec types

This commit is contained in:
2026-10-10 10:49:42 +05:00
parent 4eb657dd50
commit 8b2172c39a
19 changed files with 112 additions and 94 deletions
+12 -6
View File
@@ -166,13 +166,19 @@ round-семантика `clampByte` и trunc-семантика `clampInt` со
`$bindable`, пропс `ontogglealign` удалён; `SchemaToolView` передаёт `$bindable`, пропс `ontogglealign` удалён; `SchemaToolView` передаёт
`bind:aligned`. `bind:aligned`.
### L4. Непроверяемые `as`-касты схемы ### L4. Непроверяемые `as`-касты схемы — СДЕЛАНО (2026-10)
`SchemaToolView.svelte:97,211` - 14 field-контролов: `spec: FieldSpec` + `spec as XSpec` → точный тип спеки в
(`tool.schema as ToolSchema<Record<string, unknown>>`) и паттерн `spec as XSpec` пропсах (`spec: ColorSpec` и т.д.); каст собран в одном месте — типизированный
в 14+ field-компонентах (`CheckboxControl.svelte:13`, `ColorControl.svelte:14`, хелпер `controlFor<S>()` в `SchemaFields.svelte` (ключ kind в `FIELDS`
`GradientControl.svelte:16` и др.); двойной каст `as unknown as WorkerLike` в гарантирует подтип спеки по `fieldSpecs`).
`executor/executor.ts:35`. Касты отключают проверку соответствия spec kind'у. - `SchemaToolView`/`toolSchemaOf`: касты
`as ToolSchema<Record<string, unknown>>` удалены — дефолт дженерика
`Tool<P = Record<string, unknown>>` уже даёт нужный тип; `undefined`
обрабатывается гардами (`{#if stepSchema}` в разметке, `dims && stepSchema` в
`setStepValue`).
- `executor.ts` `as unknown as WorkerLike` оставлен намеренно: несовместимость
`onerror` (ErrorEvent vs Event) в типах lib.dom; добавлен комментарий-причина.
### L5. Хардкод slug-ов вне registry ### L5. Хардкод slug-ов вне registry
@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import type { CheckboxSpec, FieldSpec } from "$lib/registry-schema"; import type { CheckboxSpec } from "$lib/registry-schema";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: CheckboxSpec;
onchange?: (value: boolean) => void; onchange?: (value: boolean) => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as CheckboxSpec); const sp = $derived(spec);
const current = $derived( const current = $derived(
typeof value === "boolean" ? value : Boolean(sp.default), typeof value === "boolean" ? value : Boolean(sp.default),
); );
@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { ColorSpec, FieldSpec } from "$lib/registry-schema"; import type { ColorSpec } from "$lib/registry-schema";
import ColorSwatchInput from "./ColorSwatchInput.svelte"; import ColorSwatchInput from "./ColorSwatchInput.svelte";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: ColorSpec;
onchange?: (value: string) => void; onchange?: (value: string) => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as ColorSpec); const sp = $derived(spec);
const current = $derived(typeof value === "string" ? value : sp.default); const current = $derived(typeof value === "string" ? value : sp.default);
</script> </script>
@@ -1,22 +1,18 @@
<script lang="ts"> <script lang="ts">
import { t } from "$lib/i18n/t"; import { t } from "$lib/i18n/t";
import type { import type { ColorPair, ColorPairSpec } from "$lib/registry-schema";
ColorPair,
ColorPairSpec,
FieldSpec,
} from "$lib/registry-schema";
import ColorSwatchInput from "./ColorSwatchInput.svelte"; import ColorSwatchInput from "./ColorSwatchInput.svelte";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: ColorPairSpec;
onchange?: (value: ColorPair) => void; onchange?: (value: ColorPair) => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as ColorPairSpec); const sp = $derived(spec);
const current = $derived.by(() => { const current = $derived.by(() => {
const v = value as Partial<ColorPair> | undefined; const v = value as Partial<ColorPair> | undefined;
return { return {
@@ -3,22 +3,18 @@
import Button from "$lib/components/ui/Button.svelte"; import Button from "$lib/components/ui/Button.svelte";
import IconButton from "$lib/components/ui/IconButton.svelte"; import IconButton from "$lib/components/ui/IconButton.svelte";
import { t } from "$lib/i18n/t"; import { t } from "$lib/i18n/t";
import type { import type { ColorList, ColorListSpec } from "$lib/registry-schema";
ColorList,
ColorListSpec,
FieldSpec,
} from "$lib/registry-schema";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: ColorListSpec;
onchange?: (value: ColorList) => void; onchange?: (value: ColorList) => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as ColorListSpec); const sp = $derived(spec);
const current = $derived.by((): ColorList => { const current = $derived.by((): ColorList => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
const valid = value.filter( const valid = value.filter(
@@ -4,7 +4,6 @@
SIZE_PRESETS, SIZE_PRESETS,
type Dimension, type Dimension,
type DimensionSpec, type DimensionSpec,
type FieldSpec,
} from "$lib/registry-schema"; } from "$lib/registry-schema";
import ChipButton from "$lib/components/ui/ChipButton.svelte"; import ChipButton from "$lib/components/ui/ChipButton.svelte";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
@@ -12,7 +11,7 @@
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: DimensionSpec;
/** Step input dimensions: axis cap for maxFromSource fields. */ /** Step input dimensions: axis cap for maxFromSource fields. */
sourceDims?: Dimension; sourceDims?: Dimension;
onchange?: (value: Dimension, axis?: "width" | "height" | "both") => void; onchange?: (value: Dimension, axis?: "width" | "height" | "both") => void;
@@ -25,7 +24,7 @@
onchange, onchange,
}: Props = $props(); }: Props = $props();
const sp = $derived(spec as DimensionSpec); const sp = $derived(spec);
const maxW = $derived( const maxW = $derived(
sp.maxFromSource && sourceDims ? sourceDims.width : sp.max, sp.maxFromSource && sourceDims ? sourceDims.width : sp.max,
); );
@@ -1,23 +1,19 @@
<script lang="ts"> <script lang="ts">
import { t } from "$lib/i18n/t"; import { t } from "$lib/i18n/t";
import type { TextFont } from "$lib/core/domText"; import type { TextFont } from "$lib/core/domText";
import type { import type { FontStyle, FontStyleSpec } from "$lib/registry-schema";
FieldSpec,
FontStyle,
FontStyleSpec,
} from "$lib/registry-schema";
import ColorSwatchInput from "./ColorSwatchInput.svelte"; import ColorSwatchInput from "./ColorSwatchInput.svelte";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: FontStyleSpec;
onchange?: (value: FontStyle) => void; onchange?: (value: FontStyle) => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as FontStyleSpec); const sp = $derived(spec);
const current = $derived.by(() => { const current = $derived.by(() => {
const v = value as Partial<FontStyle> | undefined; const v = value as Partial<FontStyle> | undefined;
return { return {
@@ -3,17 +3,17 @@
import ColorSwatchInput from "./ColorSwatchInput.svelte"; import ColorSwatchInput from "./ColorSwatchInput.svelte";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
import { t } from "$lib/i18n/t"; import { t } from "$lib/i18n/t";
import type { FieldSpec, Gradient, GradientSpec } from "$lib/registry-schema"; import type { Gradient, GradientSpec } from "$lib/registry-schema";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: GradientSpec;
onchange?: (value: Gradient) => void; onchange?: (value: Gradient) => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as GradientSpec); const sp = $derived(spec);
const current = $derived.by(() => { const current = $derived.by(() => {
const v = value as Partial<Gradient> | undefined; const v = value as Partial<Gradient> | undefined;
return { return {
@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import type { FieldSpec, Offset, OffsetSpec } from "$lib/registry-schema"; import type { Offset, OffsetSpec } from "$lib/registry-schema";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: OffsetSpec;
onchange?: (value: Offset) => void; onchange?: (value: Offset) => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as OffsetSpec); const sp = $derived(spec);
const current = $derived.by(() => { const current = $derived.by(() => {
const v = value as Partial<Offset> | undefined; const v = value as Partial<Offset> | undefined;
return { return {
@@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import { t } from "$lib/i18n/t"; import { t } from "$lib/i18n/t";
import type { FieldSpec, Plate, PlateSpec } from "$lib/registry-schema"; import type { Plate, PlateSpec } from "$lib/registry-schema";
import ColorSwatchInput from "./ColorSwatchInput.svelte"; import ColorSwatchInput from "./ColorSwatchInput.svelte";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: PlateSpec;
onchange?: (value: Plate) => void; onchange?: (value: Plate) => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as PlateSpec); const sp = $derived(spec);
const current = $derived.by(() => { const current = $derived.by(() => {
const v = value as Partial<Plate> | undefined; const v = value as Partial<Plate> | undefined;
return { return {
@@ -1,21 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { Position9 } from "$lib/core/textdraw"; import type { Position9 } from "$lib/core/textdraw";
import { import { POSITION9_VALUES, type Position9Spec } from "$lib/registry-schema";
POSITION9_VALUES,
type FieldSpec,
type Position9Spec,
} from "$lib/registry-schema";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: Position9Spec;
onchange?: (value: Position9) => void; onchange?: (value: Position9) => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as Position9Spec); const sp = $derived(spec);
const current = $derived( const current = $derived(
typeof value === "string" && typeof value === "string" &&
(POSITION9_VALUES as readonly string[]).includes(value) (POSITION9_VALUES as readonly string[]).includes(value)
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { FieldSpec, NumberSpec, SliderSpec } from "$lib/registry-schema"; import type { NumberSpec, SliderSpec } from "$lib/registry-schema";
import { t } from "$lib/i18n/t"; import { t } from "$lib/i18n/t";
import { Minus, Plus, RotateCcw } from "@lucide/svelte"; import { Minus, Plus, RotateCcw } from "@lucide/svelte";
import IconButton from "$lib/components/ui/IconButton.svelte"; import IconButton from "$lib/components/ui/IconButton.svelte";
@@ -8,12 +8,12 @@
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: NumberSpec | SliderSpec;
onchange?: (value: number) => void; onchange?: (value: number) => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as NumberSpec | SliderSpec); const sp = $derived(spec);
const min = $derived(sp.min ?? 0); const min = $derived(sp.min ?? 0);
const max = $derived(sp.max ?? 100); const max = $derived(sp.max ?? 100);
const step = $derived(sp.step ?? 1); const step = $derived(sp.step ?? 1);
@@ -2,19 +2,19 @@
import Control from "./Control.svelte"; import Control from "./Control.svelte";
import Segmented from "$lib/components/ui/Segmented.svelte"; import Segmented from "$lib/components/ui/Segmented.svelte";
import { optionLabel } from "$lib/i18n/schema-tool-strings"; import { optionLabel } from "$lib/i18n/schema-tool-strings";
import type { FieldSpec, SegmentedSpec } from "$lib/registry-schema"; import type { SegmentedSpec } from "$lib/registry-schema";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: SegmentedSpec;
toolId: string; toolId: string;
fieldId: string; fieldId: string;
onchange?: (value: string) => void; onchange?: (value: string) => void;
} }
let { label, value, spec, toolId, fieldId, onchange }: Props = $props(); let { label, value, spec, toolId, fieldId, onchange }: Props = $props();
const sp = $derived(spec as SegmentedSpec); const sp = $derived(spec);
const current = $derived( const current = $derived(
typeof value === "string" && sp.options.some((o) => o.value === value) typeof value === "string" && sp.options.some((o) => o.value === value)
? value ? value
@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { optionLabel } from "$lib/i18n/schema-tool-strings"; import { optionLabel } from "$lib/i18n/schema-tool-strings";
import type { FieldSpec, SelectSpec } from "$lib/registry-schema"; import type { SelectSpec } from "$lib/registry-schema";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: SelectSpec;
toolId: string; toolId: string;
fieldId: string; fieldId: string;
onchange?: (value: string) => void; onchange?: (value: string) => void;
} }
let { label, value, spec, toolId, fieldId, onchange }: Props = $props(); let { label, value, spec, toolId, fieldId, onchange }: Props = $props();
const sp = $derived(spec as SelectSpec); const sp = $derived(spec);
const current = $derived( const current = $derived(
typeof value === "string" && sp.options.some((o) => o.value === value) typeof value === "string" && sp.options.some((o) => o.value === value)
? value ? value
@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import type { FieldSpec, TextSpec } from "$lib/registry-schema"; import type { TextSpec } from "$lib/registry-schema";
import Control from "./Control.svelte"; import Control from "./Control.svelte";
interface Props { interface Props {
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: TextSpec;
onchange?: (value: string) => void; onchange?: (value: string) => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
const sp = $derived(spec as TextSpec); const sp = $derived(spec);
const current = $derived(typeof value === "string" ? value : sp.default); const current = $derived(typeof value === "string" ? value : sp.default);
</script> </script>
@@ -1,8 +1,23 @@
<script lang="ts"> <script lang="ts">
import type { import type {
CheckboxSpec,
ColorListSpec,
ColorPairSpec,
ColorSpec,
Dimension, Dimension,
DimensionSpec,
FieldSpec, FieldSpec,
FieldSpecKind, FieldSpecKind,
FontStyleSpec,
GradientSpec,
NumberSpec,
OffsetSpec,
PlateSpec,
Position9Spec,
SegmentedSpec,
SelectSpec,
SliderSpec,
TextSpec,
ToolSchema, ToolSchema,
} from "$lib/registry-schema"; } from "$lib/registry-schema";
import { effectiveMax, resolveLayoutGroups } from "$lib/registry-schema"; import { effectiveMax, resolveLayoutGroups } from "$lib/registry-schema";
@@ -36,22 +51,34 @@
onchange?: (value: unknown, axis?: "width" | "height" | "both") => void; onchange?: (value: unknown, axis?: "width" | "height" | "both") => void;
} }
// The kind key of FIELDS guarantees the spec subtype at runtime (fieldSpecs
// map in registry-schema); the cast lives once here, so each control
// declares its exact spec type instead of casting internally.
type ControlFor<S extends FieldSpec> = Component<
Omit<FieldControlProps, "spec"> & { spec: S }
>;
function controlFor<S extends FieldSpec>(
c: ControlFor<S>,
): Component<FieldControlProps> {
return c as unknown as Component<FieldControlProps>;
}
const FIELDS: Record<FieldSpecKind, Component<FieldControlProps>> = { const FIELDS: Record<FieldSpecKind, Component<FieldControlProps>> = {
number: RangeControl, number: controlFor<NumberSpec | SliderSpec>(RangeControl),
slider: RangeControl, slider: controlFor<NumberSpec | SliderSpec>(RangeControl),
color: ColorControl, color: controlFor<ColorSpec>(ColorControl),
"color-pair": ColorPairControl, "color-pair": controlFor<ColorPairSpec>(ColorPairControl),
colors: ColorsControl, colors: controlFor<ColorListSpec>(ColorsControl),
select: SelectControl, select: controlFor<SelectSpec>(SelectControl),
segmented: SegmentedControl, segmented: controlFor<SegmentedSpec>(SegmentedControl),
checkbox: CheckboxControl, checkbox: controlFor<CheckboxSpec>(CheckboxControl),
text: TextControl, text: controlFor<TextSpec>(TextControl),
dimension: DimensionField, dimension: controlFor<DimensionSpec>(DimensionField),
offset: OffsetControl, offset: controlFor<OffsetSpec>(OffsetControl),
position9: PositionControl, position9: controlFor<Position9Spec>(PositionControl),
"font-style": FontStyleControl, "font-style": controlFor<FontStyleSpec>(FontStyleControl),
plate: PlateControl, plate: controlFor<PlateSpec>(PlateControl),
gradient: GradientControl, gradient: controlFor<GradientSpec>(GradientControl),
}; };
interface Props { interface Props {
@@ -42,7 +42,6 @@
applySourceDefaults, applySourceDefaults,
clampSourceAwareMaxes, clampSourceAwareMaxes,
sanitizeSchemaParams, sanitizeSchemaParams,
type ToolSchema,
} from "$lib/registry-schema"; } from "$lib/registry-schema";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { SvelteSet } from "svelte/reactivity"; import { SvelteSet } from "svelte/reactivity";
@@ -65,9 +64,7 @@
} }
let { page, tool, chainId = undefined }: Props = $props(); let { page, tool, chainId = undefined }: Props = $props();
const schema = $derived( const schema = $derived(tool.schema);
tool.schema as ToolSchema<Record<string, unknown>> | undefined,
);
const inputMode = $derived(tool.input); const inputMode = $derived(tool.input);
@@ -471,14 +468,17 @@
onadd={(id) => replaceStepTool(i, id)} onadd={(id) => replaceStepTool(i, id)}
/> />
{/snippet} {/snippet}
{@const stepSchema = toolSchemaOf(step)}
{#if stepSchema}
<SchemaFields <SchemaFields
schema={toolSchemaOf(step)} schema={stepSchema}
values={step.params} values={step.params}
toolId={step.id} toolId={step.id}
sourceDims={i === 0 ? (source ?? undefined) : stepDims[i - 1]} sourceDims={i === 0 ? (source ?? undefined) : stepDims[i - 1]}
onchange={(id, v, axis) => setStepValue(i, id, v, axis)} onchange={(id, v, axis) => setStepValue(i, id, v, axis)}
onreset={() => resetStep(i)} onreset={() => resetStep(i)}
/> />
{/if}
</StepCard> </StepCard>
{/snippet} {/snippet}
@@ -12,7 +12,6 @@ import {
defaultSchemaParams, defaultSchemaParams,
withAspectLock, withAspectLock,
type Dimension, type Dimension,
type ToolSchema,
} from "$lib/registry-schema"; } from "$lib/registry-schema";
import { SvelteMap, SvelteSet } from "svelte/reactivity"; import { SvelteMap, SvelteSet } from "svelte/reactivity";
@@ -28,7 +27,7 @@ interface StepsState {
} }
export function toolSchemaOf(step: ChainStep) { export function toolSchemaOf(step: ChainStep) {
return getTool(step.id)?.schema as ToolSchema<Record<string, unknown>>; return getTool(step.id)?.schema;
} }
function setStepValue( function setStepValue(
@@ -79,7 +78,8 @@ function setStepValue(
} }
} }
} }
const clamped = dims ? clampSourceAwareMaxes(stepSchema, next, dims) : next; const clamped =
dims && stepSchema ? clampSourceAwareMaxes(stepSchema, next, dims) : next;
s.steps = s.steps.with(index, { ...step, params: clamped }); s.steps = s.steps.with(index, { ...step, params: clamped });
} }
+2
View File
@@ -30,6 +30,8 @@ type PendingRequest = {
}; };
function defaultWorkerFactory(): WorkerLike { function defaultWorkerFactory(): WorkerLike {
// Worker's onerror is typed with ErrorEvent while WorkerLike expects Event;
// the DOM lib types are incompatible, hence the double cast.
return new Worker(new URL("./executor.worker.ts", import.meta.url), { return new Worker(new URL("./executor.worker.ts", import.meta.url), {
type: "module", type: "module",
}) as unknown as WorkerLike; }) as unknown as WorkerLike;