feat: add tool schema layout
This commit is contained in:
@@ -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 — зачистка
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user