feat(ai): add_pagination_v0 — Google-style pagination bar (57th tool)
Adds an N-tool for list/table footer pagination: row of page- number pills flanked by optional prev/next chevron buttons. Active page renders filled with the accent color, inactive pages are ghost. Long ranges collapse with "…" Google-style (always show 1 and total, plus a ±siblings window around current). Wired through all 3 paths: pen-core buildPagination + pen-mcp handler + apps/web browser shim + Nitro SERVER_BUILDERS. Both parity tests (shim-server-parity, element-tool-registry-parity) pick up the entry automatically. Handler test covers 7 cases: small range no ellipsis, 10-page ellipsis, start-edge current, accent override, show_arrows=false, total=1 single pill, bogus parent_id rejection. Also updates packages/pen-ai-skills/skills/phases/generation/ elements.md: decision tree §52, keyword triggers (pagination / page nav / 分页 / 分页条), minimal usage example. The stale- integration guard (design-prompt-elements.test.ts) now passes.
This commit is contained in:
parent
c9620b42df
commit
fa69986fc8
|
|
@ -35,6 +35,7 @@ import {
|
|||
buildMetricRow,
|
||||
buildNavChipRow,
|
||||
buildNotificationRow,
|
||||
buildPagination,
|
||||
buildPrice,
|
||||
buildProgressBar,
|
||||
buildQuoteBlock,
|
||||
|
|
@ -174,6 +175,7 @@ const SERVER_BUILDERS: Record<string, BuilderFn> = {
|
|||
add_chart_bars_v0: (a) => buildChartBars(a as Parameters<typeof buildChartBars>[0]),
|
||||
add_timeline_v0: (a) => buildTimeline(a as Parameters<typeof buildTimeline>[0]),
|
||||
add_calendar_grid_v0: (a) => buildCalendarGrid(a as Parameters<typeof buildCalendarGrid>[0]),
|
||||
add_pagination_v0: (a) => buildPagination(a as Parameters<typeof buildPagination>[0]),
|
||||
};
|
||||
|
||||
interface ExecToolBody {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import {
|
|||
buildMetricRow,
|
||||
buildNavChipRow,
|
||||
buildNotificationRow,
|
||||
buildPagination,
|
||||
buildPrice,
|
||||
buildProgressBar,
|
||||
buildQuoteBlock,
|
||||
|
|
@ -393,6 +394,11 @@ const CASES: BuilderCase[] = [
|
|||
args: {},
|
||||
build: (a) => buildCalendarGrid(a as unknown as Parameters<typeof buildCalendarGrid>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_pagination_v0',
|
||||
args: { total: 10, current: 5 },
|
||||
build: (a) => buildPagination(a as unknown as Parameters<typeof buildPagination>[0]),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import {
|
|||
buildMetricRow,
|
||||
buildNavChipRow,
|
||||
buildNotificationRow,
|
||||
buildPagination,
|
||||
buildPrice,
|
||||
buildProgressBar,
|
||||
buildQuoteBlock,
|
||||
|
|
@ -110,6 +111,7 @@ import {
|
|||
type MetricRowParams,
|
||||
type NavChipRowParams,
|
||||
type NotificationRowParams,
|
||||
type PaginationParams,
|
||||
type PriceParams,
|
||||
type ProgressBarParams,
|
||||
type QuoteBlockParams,
|
||||
|
|
@ -278,6 +280,7 @@ export const ELEMENT_SHIMS: Record<string, ElementShim> = {
|
|||
add_chart_bars_v0: wrap<ChartBarsParams>(buildChartBars),
|
||||
add_timeline_v0: wrap<TimelineParams>(buildTimeline),
|
||||
add_calendar_grid_v0: wrap<CalendarGridParams>(buildCalendarGrid),
|
||||
add_pagination_v0: wrap<PaginationParams>(buildPagination),
|
||||
};
|
||||
|
||||
export function getElementShim(name: string): ElementShim | undefined {
|
||||
|
|
|
|||
|
|
@ -159,7 +159,11 @@ Loading / placeholder:
|
|||
|
||||
43. Loading skeleton (N gray rectangles, last row ~60% width) → `add_skeleton_v0`
|
||||
|
||||
44. None match → fall through to `batch_design`
|
||||
Pagination:
|
||||
|
||||
52. Pagination bar (numbered pills + prev/next arrows, Google-style ellipses for big ranges) → `add_pagination_v0`
|
||||
|
||||
53. None match → fall through to `batch_design`
|
||||
|
||||
**Disambiguation**: if you need a ROW of 3 metrics that should NOT scroll (e.g. a stats strip inside a card), use `add_stat_grid_v0`, NOT `add_metric_row_v0`. The grid uses `fill_container` per cell so it never overflows; the metric row uses fixed-px cells + scroll wrapper.
|
||||
|
||||
|
|
@ -221,6 +225,7 @@ PREFER an element tool when the spec says any of:
|
|||
- "bar chart", "histogram skeleton", "weekly steps", "柱状图" → `add_chart_bars_v0`
|
||||
- "timeline", "activity history", "vertical stepper", "时间线", "动态" → `add_timeline_v0`
|
||||
- "calendar", "date picker grid", "month view", "日历" → `add_calendar_grid_v0`
|
||||
- "pagination", "page nav", "page numbers", "prev/next pages", "分页", "分页条" → `add_pagination_v0`
|
||||
|
||||
STILL use batch_design when:
|
||||
|
||||
|
|
@ -414,6 +419,9 @@ add_timeline_v0({
|
|||
|
||||
add_calendar_grid_v0({}) // vanilla 30-day month, Sun-start
|
||||
add_calendar_grid_v0({ days_in_month: 31, start_day_offset: 2, today: 15, selected_day: 22 })
|
||||
|
||||
add_pagination_v0({ total: 10, current: 5 }) // 1 … 4 [5] 6 … 10
|
||||
add_pagination_v0({ total: 3, current: 1, show_arrows: false }) // no prev/next
|
||||
```
|
||||
|
||||
## Composition pattern
|
||||
|
|
|
|||
|
|
@ -68,3 +68,4 @@ export { buildColorSwatch, type ColorSwatchParams } from './color-swatch.js';
|
|||
export { buildChartBars, type ChartBarsParams } from './chart-bars.js';
|
||||
export { buildTimeline, type TimelineItem, type TimelineParams } from './timeline.js';
|
||||
export { buildCalendarGrid, type CalendarGridParams } from './calendar-grid.js';
|
||||
export { buildPagination, type PaginationParams } from './pagination.js';
|
||||
|
|
|
|||
172
packages/pen-core/src/element-builders/pagination.ts
Normal file
172
packages/pen-core/src/element-builders/pagination.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
|
||||
export interface PaginationParams {
|
||||
/** Total number of pages. Clamped to >= 1. */
|
||||
total: number;
|
||||
/** 1-based index of the current page. Clamped to [1, total]. */
|
||||
current?: number;
|
||||
/**
|
||||
* How many pages to show on each side of `current` before collapsing
|
||||
* the rest into an ellipsis. Default 1 → e.g. "1 … 4 [5] 6 … 10".
|
||||
*/
|
||||
siblings?: number;
|
||||
/** Include arrow buttons on either end. Default true. */
|
||||
show_arrows?: boolean;
|
||||
/** Accent color for the active page pill. Default slate-900. */
|
||||
accent_color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination bar for list / table screens: a row of page-number
|
||||
* pills flanked by optional prev/next arrow buttons. The current
|
||||
* page is rendered as a filled pill; all other pages are ghost
|
||||
* (label only, no fill). Page-range collapse uses Google-style
|
||||
* ellipses when there are too many pages to show inline.
|
||||
*
|
||||
* Structure:
|
||||
* frame(horizontal, gap=4, alignItems=center, role='pagination')
|
||||
* ├ [icon-button chevron-left] ← if show_arrows
|
||||
* ├ pill(36×32) for first page if collapsed
|
||||
* ├ text('…') if gap ← role='pagination-ellipsis'
|
||||
* ├ pill for each page in window ← active one filled
|
||||
* ├ text('…') if gap
|
||||
* ├ pill for last page if collapsed
|
||||
* └ [icon-button chevron-right] ← if show_arrows
|
||||
*
|
||||
* Pill: `frame(36×32, cornerRadius=6, layout=horizontal, center)
|
||||
* > text(fontSize=13)`. Active pill fills with `accent_color`
|
||||
* and text is white; inactive pill has no fill, text slate-700.
|
||||
*/
|
||||
export function buildPagination(params: PaginationParams): ElementTree {
|
||||
const total = Math.max(1, Math.floor(params.total));
|
||||
const current = Math.max(1, Math.min(total, Math.floor(params.current ?? 1)));
|
||||
const siblings = Math.max(0, Math.floor(params.siblings ?? 1));
|
||||
const showArrows = params.show_arrows !== false;
|
||||
const accent = params.accent_color ?? '#0F172A';
|
||||
|
||||
// Build the visible-page window: always include 1 and total, plus a
|
||||
// [current - siblings, current + siblings] band. Insert ellipses
|
||||
// where there's a gap.
|
||||
const pages = new Set<number>([1, total, current]);
|
||||
for (let i = 1; i <= siblings; i += 1) {
|
||||
pages.add(current - i);
|
||||
pages.add(current + i);
|
||||
}
|
||||
const sorted = Array.from(pages)
|
||||
.filter((p) => p >= 1 && p <= total)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
type Entry = { kind: 'page'; n: number } | { kind: 'ellipsis' };
|
||||
const entries: Entry[] = [];
|
||||
for (let i = 0; i < sorted.length; i += 1) {
|
||||
entries.push({ kind: 'page', n: sorted[i] });
|
||||
const next = sorted[i + 1];
|
||||
if (next !== undefined && next > sorted[i] + 1) {
|
||||
entries.push({ kind: 'ellipsis' });
|
||||
}
|
||||
}
|
||||
|
||||
const children: ElementTree[] = [];
|
||||
|
||||
if (showArrows) {
|
||||
children.push({
|
||||
type: 'frame',
|
||||
name: 'Prev',
|
||||
role: 'pagination-prev',
|
||||
width: 32,
|
||||
height: 32,
|
||||
cornerRadius: 6,
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fill: [],
|
||||
children: [
|
||||
{
|
||||
type: 'icon_font',
|
||||
name: 'Prev Icon',
|
||||
iconFontName: 'chevron-left',
|
||||
iconFontFamily: 'lucide',
|
||||
width: 16,
|
||||
height: 16,
|
||||
fill: [{ type: 'solid', color: '#334155' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
for (const e of entries) {
|
||||
if (e.kind === 'ellipsis') {
|
||||
children.push({
|
||||
type: 'text',
|
||||
name: 'Ellipsis',
|
||||
role: 'pagination-ellipsis',
|
||||
content: '…',
|
||||
fontSize: 13,
|
||||
fontWeight: 400,
|
||||
fill: [{ type: 'solid', color: '#64748B' }],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const isActive = e.n === current;
|
||||
children.push({
|
||||
type: 'frame',
|
||||
name: `Page ${e.n}`,
|
||||
role: isActive ? 'pagination-page-active' : 'pagination-page',
|
||||
width: 36,
|
||||
height: 32,
|
||||
cornerRadius: 6,
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fill: isActive ? [{ type: 'solid', color: accent }] : [],
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Label',
|
||||
content: String(e.n),
|
||||
fontSize: 13,
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
fill: [{ type: 'solid', color: isActive ? '#FFFFFF' : '#334155' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (showArrows) {
|
||||
children.push({
|
||||
type: 'frame',
|
||||
name: 'Next',
|
||||
role: 'pagination-next',
|
||||
width: 32,
|
||||
height: 32,
|
||||
cornerRadius: 6,
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fill: [],
|
||||
children: [
|
||||
{
|
||||
type: 'icon_font',
|
||||
name: 'Next Icon',
|
||||
iconFontName: 'chevron-right',
|
||||
iconFontFamily: 'lucide',
|
||||
width: 16,
|
||||
height: 16,
|
||||
fill: [{ type: 'solid', color: '#334155' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Pagination',
|
||||
role: 'pagination',
|
||||
width: 'fit_content',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
|
@ -230,6 +230,7 @@ export {
|
|||
buildChartBars,
|
||||
buildTimeline,
|
||||
buildCalendarGrid,
|
||||
buildPagination,
|
||||
cjkFontFamily,
|
||||
detectCjkScript,
|
||||
type ElementTree,
|
||||
|
|
@ -304,4 +305,5 @@ export {
|
|||
type TimelineItem,
|
||||
type TimelineParams,
|
||||
type CalendarGridParams,
|
||||
type PaginationParams,
|
||||
} from './element-builders/index.js';
|
||||
|
|
|
|||
126
packages/pen-mcp/src/__tests__/add-pagination-v0.test.ts
Normal file
126
packages/pen-mcp/src/__tests__/add-pagination-v0.test.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { writeFile, unlink, readFile, mkdir } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { DESIGN_TOOL_DEFINITIONS, DESIGN_TOOL_NAMES } from '../routes/design-routes';
|
||||
import { handleAddPaginationV0 } from '../tools/add-pagination-v0';
|
||||
import { invalidateCache } from '../document-manager';
|
||||
|
||||
const TMP = join(tmpdir(), 'openpencil-add-pagination-v0');
|
||||
const EMPTY = JSON.stringify({ version: '1.0.0', children: [] });
|
||||
|
||||
async function fresh(name: string): Promise<string> {
|
||||
const fp = join(TMP, name);
|
||||
await writeFile(fp, EMPTY, 'utf-8');
|
||||
return fp;
|
||||
}
|
||||
async function readDoc(fp: string): Promise<Record<string, unknown>> {
|
||||
return JSON.parse(await readFile(fp, 'utf-8'));
|
||||
}
|
||||
function getRoot(doc: Record<string, unknown>): Record<string, unknown> {
|
||||
const pages = doc['pages'] as Array<{ children?: Record<string, unknown>[] }> | undefined;
|
||||
const top = doc['children'] as Record<string, unknown>[] | undefined;
|
||||
const root = (top ?? pages?.[0]?.children)?.[0];
|
||||
if (!root) throw new Error('no root');
|
||||
return root;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await mkdir(TMP, { recursive: true });
|
||||
});
|
||||
afterEach(async () => {
|
||||
for (const f of ['a.op']) {
|
||||
try {
|
||||
const fp = join(TMP, f);
|
||||
invalidateCache(fp);
|
||||
await unlink(fp);
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
describe('add_pagination_v0', () => {
|
||||
it('registered; required=[total]', () => {
|
||||
expect(DESIGN_TOOL_NAMES.has('add_pagination_v0')).toBe(true);
|
||||
const def = DESIGN_TOOL_DEFINITIONS.find((t) => t.name === 'add_pagination_v0');
|
||||
expect(def?.inputSchema.required).toEqual(['total']);
|
||||
});
|
||||
|
||||
it('small range (total=3) → all pages shown, no ellipsis, arrows present', async () => {
|
||||
const fp = await fresh('a.op');
|
||||
await handleAddPaginationV0({ filePath: fp, total: 3, current: 2 });
|
||||
const p = getRoot(await readDoc(fp));
|
||||
expect(p.role).toBe('pagination');
|
||||
const kids = p.children as Record<string, unknown>[];
|
||||
// prev + 3 pages + next = 5
|
||||
expect(kids.length).toBe(5);
|
||||
expect(kids[0].role).toBe('pagination-prev');
|
||||
expect(kids[4].role).toBe('pagination-next');
|
||||
// middle pill is active
|
||||
const active = kids[2];
|
||||
expect(active.role).toBe('pagination-page-active');
|
||||
const activeFill = active.fill as Array<{ color: string }>;
|
||||
expect(activeFill[0].color).toBe('#0F172A');
|
||||
});
|
||||
|
||||
it('large range with ellipsis: total=10, current=5 → 1 … 4 [5] 6 … 10', async () => {
|
||||
const fp = await fresh('a.op');
|
||||
await handleAddPaginationV0({ filePath: fp, total: 10, current: 5, show_arrows: false });
|
||||
const p = getRoot(await readDoc(fp));
|
||||
const kids = p.children as Record<string, unknown>[];
|
||||
// no arrows → 1, ellipsis, 4, 5, 6, ellipsis, 10 = 7
|
||||
expect(kids.length).toBe(7);
|
||||
const ellipsisCount = kids.filter((k) => k.role === 'pagination-ellipsis').length;
|
||||
expect(ellipsisCount).toBe(2);
|
||||
const activeCount = kids.filter((k) => k.role === 'pagination-page-active').length;
|
||||
expect(activeCount).toBe(1);
|
||||
});
|
||||
|
||||
it('current at start: total=10, current=1 → no left ellipsis', async () => {
|
||||
const fp = await fresh('a.op');
|
||||
await handleAddPaginationV0({ filePath: fp, total: 10, current: 1, show_arrows: false });
|
||||
const p = getRoot(await readDoc(fp));
|
||||
const kids = p.children as Record<string, unknown>[];
|
||||
// 1, 2, ellipsis, 10 = 4
|
||||
expect(kids.length).toBe(4);
|
||||
expect(kids[0].role).toBe('pagination-page-active');
|
||||
expect(kids[2].role).toBe('pagination-ellipsis');
|
||||
});
|
||||
|
||||
it('accent_color overrides active fill', async () => {
|
||||
const fp = await fresh('a.op');
|
||||
await handleAddPaginationV0({ filePath: fp, total: 3, current: 1, accent_color: '#4F46E5' });
|
||||
const p = getRoot(await readDoc(fp));
|
||||
const kids = p.children as Record<string, unknown>[];
|
||||
const active = kids.find((k) => k.role === 'pagination-page-active')!;
|
||||
const fill = active.fill as Array<{ color: string }>;
|
||||
expect(fill[0].color).toBe('#4F46E5');
|
||||
});
|
||||
|
||||
it('show_arrows=false drops prev/next', async () => {
|
||||
const fp = await fresh('a.op');
|
||||
await handleAddPaginationV0({ filePath: fp, total: 3, current: 1, show_arrows: false });
|
||||
const p = getRoot(await readDoc(fp));
|
||||
const kids = p.children as Record<string, unknown>[];
|
||||
expect(kids.find((k) => k.role === 'pagination-prev')).toBeUndefined();
|
||||
expect(kids.find((k) => k.role === 'pagination-next')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('total=1 → single active pill (+ arrows if default)', async () => {
|
||||
const fp = await fresh('a.op');
|
||||
await handleAddPaginationV0({ filePath: fp, total: 1 });
|
||||
const p = getRoot(await readDoc(fp));
|
||||
const kids = p.children as Record<string, unknown>[];
|
||||
// prev + 1 active + next
|
||||
expect(kids.length).toBe(3);
|
||||
expect(kids[1].role).toBe('pagination-page-active');
|
||||
});
|
||||
|
||||
it('throws on bogus parent_id AND leaves file untouched', async () => {
|
||||
const fp = await fresh('a.op');
|
||||
const before = await readFile(fp, 'utf-8');
|
||||
await expect(
|
||||
handleAddPaginationV0({ filePath: fp, total: 5, parent_id: 'nope' }),
|
||||
).rejects.toThrow(/parent_id.*not found/);
|
||||
expect(await readFile(fp, 'utf-8')).toBe(before);
|
||||
});
|
||||
});
|
||||
|
|
@ -65,6 +65,7 @@ const ELEMENT_TOOL_NAMES = [
|
|||
'add_chart_bars_v0',
|
||||
'add_timeline_v0',
|
||||
'add_calendar_grid_v0',
|
||||
'add_pagination_v0',
|
||||
];
|
||||
|
||||
describe('element tools — v0-MUST contract', () => {
|
||||
|
|
|
|||
|
|
@ -1063,4 +1063,40 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT = [
|
|||
required: ['label'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_pagination_v0',
|
||||
description:
|
||||
'Pagination bar for list/table footers: row of page-number pills flanked by optional prev/next ' +
|
||||
'arrow buttons. Active page renders as a filled pill (accent color, white text); inactive ' +
|
||||
'pages are ghost (no fill). Collapses long page ranges with "…" ellipses Google-style ' +
|
||||
'(always shows 1 and total, plus a ±siblings window around current). Use for "pagination", ' +
|
||||
'"page nav", "分页", "分页条". schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
total: { type: 'number', description: 'Total number of pages (>= 1)' },
|
||||
current: {
|
||||
type: 'number',
|
||||
description: '1-based current page (clamped to [1, total], default 1)',
|
||||
},
|
||||
siblings: {
|
||||
type: 'number',
|
||||
description: 'Pages shown on each side of current before ellipsis (default 1, min 0)',
|
||||
},
|
||||
show_arrows: {
|
||||
type: 'boolean',
|
||||
description: 'Include prev/next chevron buttons (default true)',
|
||||
},
|
||||
accent_color: {
|
||||
type: 'string',
|
||||
description: 'Hex color for active page pill (default #0F172A slate-900)',
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: ['total'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ import { handleAddColorSwatchV0 } from '../tools/add-color-swatch-v0';
|
|||
import { handleAddChartBarsV0 } from '../tools/add-chart-bars-v0';
|
||||
import { handleAddTimelineV0 } from '../tools/add-timeline-v0';
|
||||
import { handleAddCalendarGridV0 } from '../tools/add-calendar-grid-v0';
|
||||
import { handleAddPaginationV0 } from '../tools/add-pagination-v0';
|
||||
import { ELEMENT_TOOL_DEFINITIONS_BASE } from './element-tool-defs-base';
|
||||
import { ELEMENT_TOOL_DEFINITIONS_EXT } from './element-tool-defs-ext';
|
||||
|
||||
|
|
@ -198,6 +199,8 @@ export async function handleElementToolCall(name: string, a: any): Promise<strin
|
|||
return JSON.stringify(await handleAddTimelineV0(a), null, 2);
|
||||
case 'add_calendar_grid_v0':
|
||||
return JSON.stringify(await handleAddCalendarGridV0(a), null, 2);
|
||||
case 'add_pagination_v0':
|
||||
return JSON.stringify(await handleAddPaginationV0(a), null, 2);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
19
packages/pen-mcp/src/tools/add-pagination-v0.ts
Normal file
19
packages/pen-mcp/src/tools/add-pagination-v0.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { assignIdsRecursively, buildPagination, type PaginationParams } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddPaginationV0Params extends PaginationParams {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
/** Pagination bar. Tree build delegated to `buildPagination`. */
|
||||
export async function handleAddPaginationV0(
|
||||
params: AddPaginationV0Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const p = buildPagination(params);
|
||||
assignIdsRecursively(p);
|
||||
return insertElementTree({ binding: 'pagination', tree: p, ...params });
|
||||
}
|
||||
Loading…
Reference in a new issue