@kubex/zinc 1.1.95 → 1.1.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/custom-elements.json +193 -428
- package/dist/vscode.html-custom-data.json +1 -1
- package/dist/web-types.json +3 -3
- package/dist/zn.d.ts +113 -224
- package/dist/zn.min.js +331 -377
- package/docs/pages/components/flow-builder.md +17 -1
- package/docs/pages/components/page-builder.md +21 -92
- package/package.json +1 -1
- package/src/components/flow-builder/flow-builder.component.ts +16 -1
- package/src/components/flow-builder/flow-builder.test.ts +206 -1
- package/src/components/flow-builder/flow-geometry.test.ts +102 -0
- package/src/components/flow-builder/flow.types.ts +82 -15
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +163 -108
- package/src/components/flow-builder/modules/flow-step/flow-step.component.ts +2 -0
- package/src/components/page-builder/page-builder.component.ts +229 -429
- package/src/components/page-builder/page-builder.scss +10 -64
- package/src/components/page-builder/page-builder.test.ts +162 -614
- package/src/components/page-builder/page.types.ts +7 -75
- package/src/components/remarkd-editor/remarkd-editor.component.ts +37 -11
- package/src/components/remarkd-editor/remarkd-editor.test.ts +79 -0
- package/src/components/page-builder/page-tree.test.ts +0 -483
- package/src/components/page-builder/page-tree.ts +0 -329
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_WIDTHS,
|
|
3
|
-
defaultLayout,
|
|
4
|
-
generateSectionId,
|
|
5
|
-
isContainer,
|
|
6
|
-
MAX_COLUMNS,
|
|
7
|
-
MAX_CONTAINER_LEVELS,
|
|
8
|
-
MAX_SECTIONS,
|
|
9
|
-
type PageContainerLayout,
|
|
10
|
-
type PageSection,
|
|
11
|
-
type PageSectionType,
|
|
12
|
-
sanitiseWidths,
|
|
13
|
-
} from './page.types';
|
|
14
|
-
|
|
15
|
-
/** Clamps a requested column count into the supported range. */
|
|
16
|
-
function columnCount(columns: number): number {
|
|
17
|
-
return Math.min(Math.max(Math.floor(columns) || 1, 1), MAX_COLUMNS);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Drops trailing cells that hold nothing. Interior empties are meaningful and kept. */
|
|
21
|
-
export function trimTrailingEmptyCells(cells: PageSection[][]): PageSection[][] {
|
|
22
|
-
let end = cells.length;
|
|
23
|
-
while (end > 0 && cells[end - 1].length === 0) end--;
|
|
24
|
-
return cells.slice(0, end);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Pads to a whole number of rows, always leaving at least one row to drop into. */
|
|
28
|
-
export function padCells(cells: PageSection[][], columns: number): PageSection[][] {
|
|
29
|
-
const cols = columnCount(columns);
|
|
30
|
-
const remainder = cells.length % cols;
|
|
31
|
-
const pad = cells.length === 0 ? cols : remainder === 0 ? 0 : cols - remainder;
|
|
32
|
-
return [...cells, ...Array.from({length: pad}, () => [] as PageSection[])];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The canonical cell list for a container. A growable container never keeps a
|
|
37
|
-
* trailing all-empty row (the renderer supplies the `+` row itself, so keeping
|
|
38
|
-
* one would show two); a fixed container's trailing empty row is its layout and
|
|
39
|
-
* survives untouched.
|
|
40
|
-
*/
|
|
41
|
-
export function normaliseCells(cells: PageSection[][], columns: number, grow: boolean): PageSection[][] {
|
|
42
|
-
return padCells(grow ? trimTrailingEmptyCells(cells) : cells, columns);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Re-chunks the same ordered list of stacks into a different column count. Only
|
|
47
|
-
* trailing empties are dropped, so no section can be lost by a column change.
|
|
48
|
-
*/
|
|
49
|
-
export function recolumnCells(cells: PageSection[][], columns: number): PageSection[][] {
|
|
50
|
-
return padCells(trimTrailingEmptyCells(cells), columns);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Row-major view of the flat cell list, for rendering. */
|
|
54
|
-
export function cellRows(cells: PageSection[][], columns: number): PageSection[][][] {
|
|
55
|
-
const cols = columnCount(columns);
|
|
56
|
-
const rows: PageSection[][][] = [];
|
|
57
|
-
for (let i = 0; i < cells.length; i += cols) rows.push(cells.slice(i, i + cols));
|
|
58
|
-
return rows;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function containerWidths(section: PageSection, type?: PageSectionType): number[] {
|
|
62
|
-
return sanitiseWidths(section.layout?.widths ?? type?.defaultWidths ?? [...DEFAULT_WIDTHS]);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function containerColumns(section: PageSection, type?: PageSectionType): number {
|
|
66
|
-
return containerWidths(section, type).length;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function containerGrow(section: PageSection, type?: PageSectionType): boolean {
|
|
70
|
-
return section.layout?.grow ?? Boolean(type?.defaultGrow);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** A container's cells, normalised. Empty for a non-container. */
|
|
74
|
-
export function containerCells(section: PageSection, type?: PageSectionType): PageSection[][] {
|
|
75
|
-
if (!isContainer(type)) return [];
|
|
76
|
-
return normaliseCells(section.cells ?? [], containerColumns(section, type), containerGrow(section, type));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Depth-first search across top-level sections and every cell stack. */
|
|
80
|
-
export function findSection(sections: PageSection[], id: string | null): PageSection | undefined {
|
|
81
|
-
if (!id) return undefined;
|
|
82
|
-
for (const section of sections) {
|
|
83
|
-
if (section.id === id) return section;
|
|
84
|
-
for (const cell of section.cells ?? []) {
|
|
85
|
-
const hit = findSection(cell, id);
|
|
86
|
-
if (hit) return hit;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return undefined;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** New section array with `id` replaced by `patch(section)`, wherever it lives. */
|
|
93
|
-
export function patchSection(
|
|
94
|
-
sections: PageSection[],
|
|
95
|
-
id: string,
|
|
96
|
-
patch: (section: PageSection) => PageSection
|
|
97
|
-
): PageSection[] {
|
|
98
|
-
return sections.map(section => {
|
|
99
|
-
if (section.id === id) return patch(section);
|
|
100
|
-
if (!section.cells) return section;
|
|
101
|
-
return {...section, cells: section.cells.map(cell => patchSection(cell, id, patch))};
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Detaches a section from wherever it lives, returning it and the remaining tree. */
|
|
106
|
-
export function extractSection(
|
|
107
|
-
sections: PageSection[],
|
|
108
|
-
id: string
|
|
109
|
-
): [PageSection | undefined, PageSection[]] {
|
|
110
|
-
let removed: PageSection | undefined;
|
|
111
|
-
const out: PageSection[] = [];
|
|
112
|
-
for (const section of sections) {
|
|
113
|
-
if (section.id === id) {
|
|
114
|
-
removed = section;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (section.cells) {
|
|
118
|
-
const cells = section.cells.map(cell => {
|
|
119
|
-
const [hit, rest] = extractSection(cell, id);
|
|
120
|
-
if (hit) removed = hit;
|
|
121
|
-
return rest;
|
|
122
|
-
});
|
|
123
|
-
out.push({...section, cells});
|
|
124
|
-
} else {
|
|
125
|
-
out.push(section);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return [removed, out];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Inserts a section into a container's cell at a stack position. */
|
|
132
|
-
export function insertIntoCell(
|
|
133
|
-
sections: PageSection[],
|
|
134
|
-
containerId: string,
|
|
135
|
-
cellIndex: number,
|
|
136
|
-
insertIndex: number,
|
|
137
|
-
section: PageSection,
|
|
138
|
-
columns: number
|
|
139
|
-
): PageSection[] {
|
|
140
|
-
return patchSection(sections, containerId, container => {
|
|
141
|
-
const cells = (container.cells ?? []).map(cell => [...cell]);
|
|
142
|
-
const target = Number.isFinite(cellIndex) ? Math.max(0, Math.floor(cellIndex)) : 0;
|
|
143
|
-
while (cells.length <= target) cells.push([]);
|
|
144
|
-
const cell = cells[target];
|
|
145
|
-
cell.splice(Math.max(0, Math.min(insertIndex, cell.length)), 0, section);
|
|
146
|
-
return {...container, cells: padCells(cells, columns)};
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Nesting level of the container with this id: 1 at the top level, 2 inside a
|
|
152
|
-
* cell, 0 when not found. Only containers have cells, so every cell level is a
|
|
153
|
-
* container level.
|
|
154
|
-
*/
|
|
155
|
-
export function containerDepth(sections: PageSection[], containerId: string, depth = 1): number {
|
|
156
|
-
for (const section of sections) {
|
|
157
|
-
if (section.id === containerId) return depth;
|
|
158
|
-
for (const cell of section.cells ?? []) {
|
|
159
|
-
const hit = containerDepth(cell, containerId, depth + 1);
|
|
160
|
-
if (hit) return hit;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return 0;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Deep copy with a fresh id for the section and every descendant. */
|
|
167
|
-
export function cloneWithNewIds(section: PageSection): PageSection {
|
|
168
|
-
const reid = (s: PageSection): PageSection => ({
|
|
169
|
-
...structuredClone(s),
|
|
170
|
-
id: generateSectionId(),
|
|
171
|
-
...(s.cells ? {cells: s.cells.map(cell => cell.map(reid))} : {}),
|
|
172
|
-
});
|
|
173
|
-
return reid(section);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Resolves a type key to its registered type — the registry's `get`. */
|
|
177
|
-
export type TypeLookup = (type: string) => PageSectionType | undefined;
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Height of a container's subtree: 1 for a container with no nested containers,
|
|
181
|
-
* otherwise 1 + the tallest nested container's height. 0 for a non-container, so
|
|
182
|
-
* it composes with containerDepth to bound how deep a moved subtree would reach:
|
|
183
|
-
* dropping a section into a container at depth d puts the section's own deepest
|
|
184
|
-
* descendant at d + containerHeight(section).
|
|
185
|
-
*/
|
|
186
|
-
export function containerHeight(section: PageSection): number {
|
|
187
|
-
if (!section.cells) return 0;
|
|
188
|
-
const nested = section.cells.flat().map(containerHeight);
|
|
189
|
-
return 1 + Math.max(0, ...nested);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Re-applies each container's own grow/columns normalisation throughout the
|
|
194
|
-
* tree, so a growable container never carries a trailing all-empty row once
|
|
195
|
-
* committed — not only when read out via containerCells. A section whose
|
|
196
|
-
* current type isn't a registered container is left untouched, preserving its
|
|
197
|
-
* cells verbatim per the unknown-type contract.
|
|
198
|
-
*/
|
|
199
|
-
export function normaliseGrowth(sections: PageSection[], lookup: TypeLookup): PageSection[] {
|
|
200
|
-
return sections.map(section => {
|
|
201
|
-
if (!section.cells) return section;
|
|
202
|
-
const type = lookup(section.type);
|
|
203
|
-
if (!isContainer(type)) return section;
|
|
204
|
-
const cells = normaliseCells(section.cells, containerColumns(section, type), containerGrow(section, type))
|
|
205
|
-
.map(cell => normaliseGrowth(cell, lookup));
|
|
206
|
-
return {...section, cells};
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Rewrites the pre-cells `children` shape as `cells`. The old grid was always
|
|
212
|
-
* DEFAULT_WIDTHS wide, and the slot count was the layout, so cells are padded
|
|
213
|
-
* out to the declared slot count, then rounded up to a whole number of
|
|
214
|
-
* DEFAULT_WIDTHS-wide rows; never trimmed.
|
|
215
|
-
*/
|
|
216
|
-
export function migrateSection(section: PageSection, type?: PageSectionType): PageSection {
|
|
217
|
-
if (!section.children) return section;
|
|
218
|
-
const {children, ...rest} = section;
|
|
219
|
-
const slots = type?.slots ?? children.length;
|
|
220
|
-
const cells = Array.from({length: slots}, (_, i) => {
|
|
221
|
-
const child = children[i];
|
|
222
|
-
return child && typeof child.type === 'string' ? [child] : [];
|
|
223
|
-
});
|
|
224
|
-
return {
|
|
225
|
-
...rest,
|
|
226
|
-
layout: section.layout ?? {widths: [...DEFAULT_WIDTHS], grow: false},
|
|
227
|
-
cells: padCells(cells, DEFAULT_WIDTHS.length),
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function isSectionLike(value: unknown): value is PageSection {
|
|
232
|
-
return Boolean(value) && typeof value === 'object' && typeof (value as PageSection).type === 'string';
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Normalises externally supplied sections: migrates the old shape, gives every
|
|
237
|
-
* section a unique id, drops malformed entries, sanitises layouts, caps nesting
|
|
238
|
-
* and clamps sizes. Pure — warnings are returned for the caller to log.
|
|
239
|
-
*/
|
|
240
|
-
export function normaliseSections(
|
|
241
|
-
sections: unknown,
|
|
242
|
-
lookup: TypeLookup
|
|
243
|
-
): {sections: PageSection[]; warnings: string[]} {
|
|
244
|
-
const warnings: string[] = [];
|
|
245
|
-
const seen = new Set<string>();
|
|
246
|
-
// A running admitted-section budget shared across every level, so 500 containers
|
|
247
|
-
// of 500 cells each can't slip past MAX_SECTIONS by hiding depth in nesting —
|
|
248
|
-
// only the top-level count was ever bounded before.
|
|
249
|
-
let remaining = MAX_SECTIONS;
|
|
250
|
-
let budgetExceeded = false;
|
|
251
|
-
|
|
252
|
-
const walk = (raw: PageSection, depth: number): PageSection | undefined => {
|
|
253
|
-
if (remaining <= 0) {
|
|
254
|
-
budgetExceeded = true;
|
|
255
|
-
return undefined;
|
|
256
|
-
}
|
|
257
|
-
remaining--;
|
|
258
|
-
const type = lookup(raw.type);
|
|
259
|
-
const migrated = migrateSection(raw, type);
|
|
260
|
-
const id = !migrated.id || seen.has(migrated.id) ? generateSectionId() : migrated.id;
|
|
261
|
-
seen.add(id);
|
|
262
|
-
|
|
263
|
-
const out: PageSection = {
|
|
264
|
-
id,
|
|
265
|
-
type: migrated.type,
|
|
266
|
-
label: migrated.label,
|
|
267
|
-
data: structuredClone(migrated.data ?? {}),
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
if (!migrated.cells) return out;
|
|
271
|
-
|
|
272
|
-
// A container past the cap keeps its identity but not its contents, so a
|
|
273
|
-
// malformed page loses as little as possible without unbounded recursion.
|
|
274
|
-
if (depth > MAX_CONTAINER_LEVELS) {
|
|
275
|
-
warnings.push(`container "${migrated.type}" exceeds ${MAX_CONTAINER_LEVELS} nesting levels; its cells were dropped`);
|
|
276
|
-
out.layout = sanitiseLayout(migrated.layout, type, warnings);
|
|
277
|
-
out.cells = [];
|
|
278
|
-
return out;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const layout = sanitiseLayout(migrated.layout, type, warnings);
|
|
282
|
-
// Slice to a whole number of rows (not MAX_SECTIONS itself) so the padding
|
|
283
|
-
// below is a no-op and the clamped length never overshoots MAX_SECTIONS.
|
|
284
|
-
const cols = layout.widths.length;
|
|
285
|
-
const maxCells = Math.floor(MAX_SECTIONS / cols) * cols;
|
|
286
|
-
let rawCells = migrated.cells;
|
|
287
|
-
if (rawCells.length > maxCells) {
|
|
288
|
-
warnings.push(`container "${migrated.type}" declared ${rawCells.length} cells; keeping the first ${maxCells}`);
|
|
289
|
-
rawCells = rawCells.slice(0, maxCells);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
out.layout = layout;
|
|
293
|
-
out.cells = normaliseCells(
|
|
294
|
-
rawCells.map(cell => (Array.isArray(cell)
|
|
295
|
-
? cell.filter(isSectionLike).map(child => walk(child, depth + 1)).filter(isDefined)
|
|
296
|
-
: [])),
|
|
297
|
-
layout.widths.length,
|
|
298
|
-
layout.grow
|
|
299
|
-
);
|
|
300
|
-
return out;
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const incoming = (Array.isArray(sections) ? sections : []).filter(isSectionLike);
|
|
304
|
-
const kept = incoming.map(section => walk(section, 1)).filter(isDefined);
|
|
305
|
-
if (budgetExceeded) {
|
|
306
|
-
warnings.push(`page tree has more than ${MAX_SECTIONS} sections including nested ones; the rest were dropped`);
|
|
307
|
-
}
|
|
308
|
-
return {sections: kept, warnings};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function isDefined<T>(value: T | undefined): value is T {
|
|
312
|
-
return value !== undefined;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function sanitiseLayout(
|
|
316
|
-
layout: PageContainerLayout | undefined,
|
|
317
|
-
type: PageSectionType | undefined,
|
|
318
|
-
warnings: string[]
|
|
319
|
-
): PageContainerLayout {
|
|
320
|
-
const seeded = defaultLayout(type);
|
|
321
|
-
if (!layout) return seeded;
|
|
322
|
-
const widths = sanitiseWidths(layout.widths);
|
|
323
|
-
const raw = layout.widths;
|
|
324
|
-
const unchanged = Array.isArray(raw) && raw.length === widths.length && raw.every((w, i) => w === widths[i]);
|
|
325
|
-
if (!unchanged) {
|
|
326
|
-
warnings.push(`container layout had unusable widths; corrected to ${widths.join(' ')}`);
|
|
327
|
-
}
|
|
328
|
-
return {widths, grow: Boolean(layout.grow)};
|
|
329
|
-
}
|