feat: add tool schema layout

This commit is contained in:
2026-09-05 17:30:34 +05:00
parent cad9bf6cfd
commit d48262e482
5 changed files with 151 additions and 27 deletions
+12 -2
View File
@@ -20,7 +20,12 @@
> preview
> научен применять **генераторы** (`executeGenerate`, кнопка Generate) и
> рендерить все kinds схемы (slider/number/color/select/checkbox/dimension/
> color-pair/offset/position9/font-style/plate/gradient).
> color-pair/offset/position9/font-style/plate/gradient). **Пер-инструмент
> раскладка `schema.layout` реализована** (шаг 32): тип `ToolSchemaLayout`
> {groups: [{title?, cols?, fields}]}, рендер групп в `SchemaFields.svelte`
> (заголовок группы + сетка колонок, неупомянутые поля — в общей группе),
> пилот — `add-text` (группы Text/Placement/Plate).
> Следующее: шаг 33 — UI-макет каждого переведённого инструмента (ревью по одному).
>
> Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts`
> (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate)
@@ -411,7 +416,12 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
31. По мере перевода инструментов в Фазе 2-3 — рендер схемы (из Фазы 1)
покрывает их автоматически; составные виджеты (dimension, color-pair,
offset, position9, font-style, plate, gradient) — по одному, каждый с ревью.
32. Пер-инструмент layout (`schema.layout`): группировка полей рамки и т.п.
32. Пер-инструмент layout (`schema.layout`) — **реализован** ✔.
`ToolSchemaLayout.groups: { title?, cols?, fields[] }` в registry-schema;
`SchemaFields.svelte` рендерит группы (подпись + сетка `1..n` колонок,
неупомянутые поля — в общей группе в конце, без заголовка). Пилот —
`add-text-png` (Text: text+style; Placement: position+margin; Plate).
Тесты: schema-layout в registry-schema.test.ts; полные запуски чисты.
33. Для каждого переведённого инструмента — UI-макет, принимается отдельно.
### Фаза 5 — зачистка
+65 -3
View File
@@ -56,17 +56,62 @@
.replace(/[_-]+/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase());
}
interface LayoutGroup {
key: string;
title?: string;
cols: number;
fields: string[];
}
const layoutGroups = $derived.by((): LayoutGroup[] => {
const all = Object.keys(schema.fields);
const groups = schema.layout?.groups ?? [];
const used: Record<string, true> = {};
const named = groups
.map((g, gi) => {
const fields = g.fields.filter((f) => {
if (used[f] || !(f in schema.fields)) return false;
used[f] = true;
return true;
});
return {
key: `${g.title ?? "group"}-${gi}`,
title: g.title,
cols: Math.max(1, Math.trunc(g.cols ?? 1)),
fields,
} satisfies LayoutGroup;
})
.filter((g) => g.fields.length > 0);
const rest = all.filter((f) => !used[f]);
if (rest.length > 0) {
named.push({ key: "__default", title: undefined, cols: 1, fields: rest });
}
return named;
});
</script>
{#each Object.entries(schema.fields) as [id, field] (id)}
{@const Control = FIELDS[field.spec.kind]}
{#each layoutGroups as group (group.key)}
{#if group.title}
<span class="group-title">{group.title}</span>
{/if}
<div
class="group-fields"
style:grid-template-columns={group.cols > 1
? `repeat(${group.cols}, minmax(0, 1fr))`
: undefined}
>
{#each group.fields as id (id)}
{@const Control = FIELDS[schema.fields[id].spec.kind]}
<Control
label={labelOf(id)}
value={values[id]}
spec={field.spec}
spec={schema.fields[id].spec}
onchange={(v) => onchange(id, v)}
/>
{/each}
</div>
{/each}
<div class="panel-foot">
<button class="reset-btn" onclick={onreset}>
@@ -76,6 +121,23 @@
</div>
<style>
.group-title {
display: block;
margin-top: var(--space-l);
margin-bottom: var(--space-s);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
.group-title:first-child {
margin-top: 0;
}
.group-fields {
display: grid;
grid-template-columns: 1fr;
column-gap: var(--space-l);
}
.panel-foot {
display: flex;
justify-content: space-between;
+13 -2
View File
@@ -13,7 +13,8 @@ interface AddTextParams {
plate: Plate;
}
const addTextSchema = toolSchema<AddTextParams>({
const addTextSchema = toolSchema<AddTextParams>(
{
text: field.text({ default: "Hello!", placeholder: "Your text" }),
style: field.fontStyle({
min: 8,
@@ -26,7 +27,17 @@ const addTextSchema = toolSchema<AddTextParams>({
position: field.position9({ default: "bottom-right" }),
margin: field.slider({ min: 0, max: 200, step: 1, default: 24 }),
plate: field.plate({ enabled: false, color: "#000000", opacity: 60 }),
});
},
{
layout: {
groups: [
{ title: "Text", fields: ["text", "style"] },
{ title: "Placement", fields: ["position", "margin"] },
{ title: "Plate", fields: ["plate"] },
],
},
},
);
const addText: ToolEntry<AddTextParams> = {
id: "add-text-png",
+28 -1
View File
@@ -26,7 +26,10 @@ const frameSchema = toolSchema<FrameParams>(
],
}),
},
{ layout: { group: "frame", cols: 2 }, label: "Frame" },
{
layout: { groups: [{ title: "Frame", fields: ["thickness", "color"] }] },
label: "Frame",
},
);
describe("registry-schema: дефолты из схемы", () => {
@@ -40,6 +43,30 @@ describe("registry-schema: дефолты из схемы", () => {
});
});
describe("registry-schema: раскладка (schema.layout)", () => {
it("toolSchema сохраняет группы полей", () => {
expect(frameSchema.layout).toEqual({
groups: [{ title: "Frame", fields: ["thickness", "color"] }],
});
});
it("обе схемы без layout не добавляют layout", () => {
const plain = toolSchema<FrameParams>({
thickness: field.slider({ min: 1, max: 500, default: 5 }),
color: field.color({ default: "#000000" }),
enabled: field.checkbox({ default: true }),
count: field.select({
default: "two",
options: [
{ value: "one", label: "One" },
{ value: "two", label: "Two" },
],
}),
});
expect(plain.layout).toBeUndefined();
});
});
describe("registry-schema: санитайз", () => {
it("пропускает валидные значения", () => {
const out = sanitizeSchemaParams(frameSchema, {
+16 -2
View File
@@ -205,16 +205,30 @@ export interface Field<T> {
__fieldT?: T;
}
export interface ToolLayoutGroup {
/** Подпись группы (пустая группа не рендерится). */
title?: string;
/** Количество колонок сетки внутри группы (по умолчанию 1). */
cols?: number;
/** Имена полей схемы, попадающих в группу. */
fields: string[];
}
export interface ToolSchemaLayout {
/** Группы полей виджета. Неупомянутые поля — в общей группе в конце. */
groups: ToolLayoutGroup[];
}
export interface ToolSchemaMeta {
/** Пер-инструмент раскладка полей (группировка/колонки). */
layout?: { group?: string; cols?: number };
layout?: ToolSchemaLayout;
/** Короткая подпись инструмента для нового UI (необязательно). */
label?: string;
}
export interface ToolSchema<P> {
fields: { [K in keyof P]: Field<P[K]> };
layout?: { group?: string; cols?: number };
layout?: ToolSchemaLayout;
label?: string;
}