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 > preview
> научен применять **генераторы** (`executeGenerate`, кнопка Generate) и > научен применять **генераторы** (`executeGenerate`, кнопка Generate) и
> рендерить все kinds схемы (slider/number/color/select/checkbox/dimension/ > рендерить все 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` > Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts`
> (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate) > (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate)
@@ -411,7 +416,12 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
31. По мере перевода инструментов в Фазе 2-3 — рендер схемы (из Фазы 1) 31. По мере перевода инструментов в Фазе 2-3 — рендер схемы (из Фазы 1)
покрывает их автоматически; составные виджеты (dimension, color-pair, покрывает их автоматически; составные виджеты (dimension, color-pair,
offset, position9, font-style, plate, gradient) — по одному, каждый с ревью. 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-макет, принимается отдельно. 33. Для каждого переведённого инструмента — UI-макет, принимается отдельно.
### Фаза 5 — зачистка ### Фаза 5 — зачистка
+65 -3
View File
@@ -56,16 +56,61 @@
.replace(/[_-]+/g, " ") .replace(/[_-]+/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase()); .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> </script>
{#each Object.entries(schema.fields) as [id, field] (id)} {#each layoutGroups as group (group.key)}
{@const Control = FIELDS[field.spec.kind]} {#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 <Control
label={labelOf(id)} label={labelOf(id)}
value={values[id]} value={values[id]}
spec={field.spec} spec={schema.fields[id].spec}
onchange={(v) => onchange(id, v)} onchange={(v) => onchange(id, v)}
/> />
{/each}
</div>
{/each} {/each}
<div class="panel-foot"> <div class="panel-foot">
@@ -76,6 +121,23 @@
</div> </div>
<style> <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 { .panel-foot {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
+13 -2
View File
@@ -13,7 +13,8 @@ interface AddTextParams {
plate: Plate; plate: Plate;
} }
const addTextSchema = toolSchema<AddTextParams>({ const addTextSchema = toolSchema<AddTextParams>(
{
text: field.text({ default: "Hello!", placeholder: "Your text" }), text: field.text({ default: "Hello!", placeholder: "Your text" }),
style: field.fontStyle({ style: field.fontStyle({
min: 8, min: 8,
@@ -26,7 +27,17 @@ const addTextSchema = toolSchema<AddTextParams>({
position: field.position9({ default: "bottom-right" }), position: field.position9({ default: "bottom-right" }),
margin: field.slider({ min: 0, max: 200, step: 1, default: 24 }), margin: field.slider({ min: 0, max: 200, step: 1, default: 24 }),
plate: field.plate({ enabled: false, color: "#000000", opacity: 60 }), 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> = { const addText: ToolEntry<AddTextParams> = {
id: "add-text-png", 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: дефолты из схемы", () => { 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: санитайз", () => { describe("registry-schema: санитайз", () => {
it("пропускает валидные значения", () => { it("пропускает валидные значения", () => {
const out = sanitizeSchemaParams(frameSchema, { const out = sanitizeSchemaParams(frameSchema, {
+16 -2
View File
@@ -205,16 +205,30 @@ export interface Field<T> {
__fieldT?: T; __fieldT?: T;
} }
export interface ToolLayoutGroup {
/** Подпись группы (пустая группа не рендерится). */
title?: string;
/** Количество колонок сетки внутри группы (по умолчанию 1). */
cols?: number;
/** Имена полей схемы, попадающих в группу. */
fields: string[];
}
export interface ToolSchemaLayout {
/** Группы полей виджета. Неупомянутые поля — в общей группе в конце. */
groups: ToolLayoutGroup[];
}
export interface ToolSchemaMeta { export interface ToolSchemaMeta {
/** Пер-инструмент раскладка полей (группировка/колонки). */ /** Пер-инструмент раскладка полей (группировка/колонки). */
layout?: { group?: string; cols?: number }; layout?: ToolSchemaLayout;
/** Короткая подпись инструмента для нового UI (необязательно). */ /** Короткая подпись инструмента для нового UI (необязательно). */
label?: string; label?: string;
} }
export interface ToolSchema<P> { export interface ToolSchema<P> {
fields: { [K in keyof P]: Field<P[K]> }; fields: { [K in keyof P]: Field<P[K]> };
layout?: { group?: string; cols?: number }; layout?: ToolSchemaLayout;
label?: string; label?: string;
} }