@praxisui/table 10.0.0-rc.4 → 10.0.0-rc.40

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.
@@ -0,0 +1,2173 @@
1
+ import { validateRichContentDocument, isSafeRichContentClassName } from '@praxisui/rich-content';
2
+ import { resolveColumnTypeFromFieldDefinition, deepMerge, isPraxisPresentationVisualizationTableSafe, resolveTextMaskFormatFromFieldDefinition } from '@praxisui/core/schema';
3
+ import { uiLayoutJsonIdentity, cloneBoundedUiLayoutJsonDocument, derivePageChildRuntimeId } from '@praxisui/core';
4
+
5
+ const ORIGIN = '__praxisAuthoredDisabled';
6
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
7
+ const isAction = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
8
+ const hasRuntimeState = (action) => Object.keys(action).some(key => key.startsWith('__praxis'));
9
+ function authoredDisabled(action) {
10
+ const origin = action[ORIGIN];
11
+ if (isAction(origin) && typeof origin['present'] === 'boolean'
12
+ && (!origin['present'] || typeof origin['value'] === 'boolean')) {
13
+ return origin;
14
+ }
15
+ return {
16
+ present: typeof action['disabled'] === 'boolean',
17
+ value: typeof action['disabled'] === 'boolean' ? action['disabled'] : undefined,
18
+ // Old materializations do not carry enough evidence to remove a denial safely.
19
+ ...(hasRuntimeState(action) && action['disabled'] === true ? { ambiguous: true } : {}),
20
+ };
21
+ }
22
+ /** Internal runtime bookkeeping. It survives clones but is never part of an authored action. */
23
+ function withActionAvailability(action, disabled, source = action) {
24
+ const origin = authoredDisabled(source);
25
+ return { ...action, disabled: origin.value === true || disabled, [ORIGIN]: origin };
26
+ }
27
+ function identity(action) {
28
+ if (typeof action['id'] === 'string' && action['id'].trim())
29
+ return `id:${action['id']}`;
30
+ if (typeof action['action'] === 'string' && action['action'].trim())
31
+ return `action:${action['action']}`;
32
+ return undefined;
33
+ }
34
+ /** Projects only canonical action slots, never arbitrary business payloads or callback values. */
35
+ function projectTableActionAuthoring(config, authored, report) {
36
+ const hasAuthoredBaseline = authored !== undefined && authored !== null;
37
+ function actions(value, original, path) {
38
+ if (!Array.isArray(value))
39
+ return value;
40
+ const originals = Array.isArray(original) ? original.filter(isAction) : [];
41
+ const projected = [];
42
+ value.forEach((item, index) => {
43
+ if (!isAction(item)) {
44
+ projected.push(item);
45
+ return;
46
+ }
47
+ const key = identity(item);
48
+ const matches = key ? originals.filter(candidate => identity(candidate) === key) : [];
49
+ // No label matching, positional matching or guessing across duplicate identities.
50
+ const source = matches.length === 1 && value.filter(candidate => isAction(candidate) && identity(candidate) === key).length === 1
51
+ ? matches[0] : undefined;
52
+ const isRuntimeSynthesizedOnly = Boolean(item['__praxisDiscoveredAction']) ||
53
+ Boolean(item['__praxisBulkSyncAction']) ||
54
+ (hasAuthoredBaseline && Boolean(item['__praxisCollectionCreateAction']));
55
+ if (isRuntimeSynthesizedOnly && !source) {
56
+ return;
57
+ }
58
+ const result = { ...item };
59
+ if (hasRuntimeState(item)) {
60
+ const origin = source && !hasRuntimeState(source) ? authoredDisabled(source) : authoredDisabled(item);
61
+ if (origin.present)
62
+ result['disabled'] = origin.value;
63
+ else
64
+ delete result['disabled'];
65
+ if (origin.ambiguous)
66
+ report?.({
67
+ level: 'warning', code: 'table.action.disabled.origin.unknown',
68
+ message: 'Review the disabled setting: this legacy action contains runtime availability without its authored origin. The existing denial was preserved.',
69
+ path: `${path}[${index}].disabled`,
70
+ });
71
+ for (const name of Object.keys(result))
72
+ if (name.startsWith('__praxis'))
73
+ delete result[name];
74
+ }
75
+ if (hasOwn(item, 'children'))
76
+ result['children'] = actions(item['children'], source?.['children'], `${path}[${index}].children`);
77
+ projected.push(result);
78
+ });
79
+ return projected;
80
+ }
81
+ const result = { ...config };
82
+ if (config?.toolbar)
83
+ result.toolbar = {
84
+ ...config.toolbar,
85
+ ...(hasOwn(config.toolbar, 'actions') ? { actions: actions(config.toolbar.actions, authored?.toolbar?.actions, 'config.toolbar.actions') } : {}),
86
+ };
87
+ if (config?.actions) {
88
+ result.actions = { ...config.actions };
89
+ for (const slot of ['row', 'bulk']) {
90
+ const group = config.actions[slot];
91
+ if (group)
92
+ result.actions[slot] = {
93
+ ...group,
94
+ ...(hasOwn(group, 'actions') ? { actions: actions(group.actions, authored?.actions?.[slot]?.actions, `config.actions.${slot}.actions`) } : {}),
95
+ };
96
+ }
97
+ }
98
+ return result;
99
+ }
100
+ /** Internal adapter: project before canonical normalization discards runtime evidence. */
101
+ function projectTableEditorActionInput(raw, authored) {
102
+ if (!isAction(raw))
103
+ return raw;
104
+ if (raw['kind'] === 'praxis.table.editor' && isAction(raw['config'])) {
105
+ return { ...raw, config: projectTableActionAuthoring(raw['config'], authored) };
106
+ }
107
+ return projectTableActionAuthoring(raw, authored);
108
+ }
109
+
110
+ const FORBIDDEN_ENCODED_PATH_PART = /%(?:25)*(?:2e|2f|5c)/i;
111
+ const FORBIDDEN_RAW_PATH_PART = /\\/;
112
+ const FORBIDDEN_CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
113
+ const FORBIDDEN_DOT_SEGMENT = /(?:^|\/)\.{1,2}(?:\/|$)/;
114
+ const ABSOLUTE_OR_SCHEME = /^(?:\/\/|[a-z][a-z0-9+.-]*:|[a-z]:[\\/])/i;
115
+ function isTableDetailResourcePathTemplateSafe(value) {
116
+ const raw = String(value || '').trim();
117
+ const path = raw.split(/[?#]/, 1)[0];
118
+ return !!raw && !ABSOLUTE_OR_SCHEME.test(raw)
119
+ && !FORBIDDEN_CONTROL_CHARACTER.test(raw)
120
+ && !FORBIDDEN_ENCODED_PATH_PART.test(path)
121
+ && !FORBIDDEN_RAW_PATH_PART.test(path)
122
+ && !FORBIDDEN_DOT_SEGMENT.test(path);
123
+ }
124
+ function canonicalPath(value, baseHref, allowWildcard = false) {
125
+ const raw = String(value || '').trim();
126
+ if (!isTableDetailResourcePathTemplateSafe(raw))
127
+ return null;
128
+ let wildcardSentinel = '__PRAXIS_DETAIL_WILDCARD__';
129
+ while (raw.includes(wildcardSentinel)) {
130
+ wildcardSentinel += '_';
131
+ }
132
+ const prepared = allowWildcard ? raw.replace(/\*/g, wildcardSentinel) : raw;
133
+ try {
134
+ const parsed = new URL(prepared, baseHref);
135
+ if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password)
136
+ return null;
137
+ const path = parsed.pathname
138
+ .replace(/^\/+/, '')
139
+ .replace(new RegExp(wildcardSentinel, 'g'), '*');
140
+ return { origin: parsed.origin, path, url: parsed.href };
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ }
146
+ function wildcardToRegExp(pattern) {
147
+ let source = '^';
148
+ for (let index = 0; index < pattern.length; index += 1) {
149
+ const character = pattern[index];
150
+ if (character === '*') {
151
+ if (pattern[index + 1] === '*') {
152
+ source += '.*';
153
+ index += 1;
154
+ }
155
+ else {
156
+ source += '[^/]+';
157
+ }
158
+ }
159
+ else {
160
+ source += character.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
161
+ }
162
+ }
163
+ return new RegExp(`${source}$`);
164
+ }
165
+ /** Resolves the URL exactly as the browser will, then applies same-origin and path policy. */
166
+ function resolveTableDetailResourcePath(value, allowList, baseHref, trustedOrigin) {
167
+ let base;
168
+ let trusted;
169
+ try {
170
+ base = new URL(baseHref);
171
+ trusted = new URL(trustedOrigin);
172
+ }
173
+ catch {
174
+ return { ok: false, reason: 'invalid' };
175
+ }
176
+ const candidate = canonicalPath(value, base.href);
177
+ if (!candidate)
178
+ return { ok: false, reason: 'invalid' };
179
+ if (base.origin !== trusted.origin || candidate.origin !== trusted.origin) {
180
+ return { ok: false, reason: 'origin' };
181
+ }
182
+ const allowed = allowList.some((entry) => {
183
+ const pattern = canonicalPath(entry, base.href, true);
184
+ return !!pattern
185
+ && pattern.origin === trusted.origin
186
+ && wildcardToRegExp(pattern.path).test(candidate.path);
187
+ });
188
+ return allowed
189
+ ? { ok: true, url: candidate.url, normalizedPath: candidate.path }
190
+ : { ok: false, reason: 'allowlist' };
191
+ }
192
+
193
+ function isObject(value) {
194
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
195
+ }
196
+ function applyTableDetailAuthoringOperation(config, operationId, input) {
197
+ if (!isObject(input) || !Object.keys(input).length)
198
+ throw new Error('Detail input must be a non-empty object.');
199
+ const merge = (source, patch) => {
200
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch))
201
+ return structuredClone(patch);
202
+ const next = source && typeof source === 'object' && !Array.isArray(source) ? { ...source } : {};
203
+ for (const key of Object.keys(patch))
204
+ Object.defineProperty(next, key, {
205
+ value: merge(Object.prototype.hasOwnProperty.call(next, key) ? next[key] : undefined, patch[key]),
206
+ enumerable: true, configurable: true, writable: true,
207
+ });
208
+ return next;
209
+ };
210
+ let next = { ...config, behavior: { ...config.behavior } };
211
+ let command = {};
212
+ if (operationId === 'expansion.configure') {
213
+ next.behavior.expansion = merge(config.behavior?.expansion, input);
214
+ if (input.enabled === true)
215
+ command = { enabled: true, placement: 'row' };
216
+ }
217
+ else if (operationId === 'detail.configure') {
218
+ if (input.presentation !== undefined && !isObject(input.presentation))
219
+ throw new Error('Invalid detail presentation input.');
220
+ next.behavior.detail = merge(config.behavior?.detail, input);
221
+ command = {
222
+ placement: input.presentation?.placement,
223
+ initiallyCollapsed: input.presentation?.initiallyCollapsed,
224
+ bottomStackPosition: input.presentation?.bottomStackPosition,
225
+ emptyBehavior: input.presentation?.emptyBehavior,
226
+ enabled: input.enabled,
227
+ };
228
+ }
229
+ else if (operationId === 'detail.presentation.configure') {
230
+ next.behavior.detail = { ...config.behavior?.detail, presentation: merge(config.behavior?.detail?.presentation, input) };
231
+ command = {
232
+ placement: input.placement,
233
+ initiallyCollapsed: input.initiallyCollapsed,
234
+ bottomStackPosition: input.bottomStackPosition,
235
+ emptyBehavior: input.emptyBehavior,
236
+ };
237
+ }
238
+ else {
239
+ next.behavior.detail = { ...config.behavior?.detail, source: merge(config.behavior?.detail?.source, input) };
240
+ }
241
+ if (((operationId === 'detail.configure' || operationId === 'expansion.configure') && input.enabled !== undefined && typeof input.enabled !== 'boolean')
242
+ || (command.enabled !== undefined && typeof command.enabled !== 'boolean')
243
+ || (command.placement !== undefined && !['row', 'bottom'].includes(command.placement))
244
+ || (command.initiallyCollapsed !== undefined && typeof command.initiallyCollapsed !== 'boolean')
245
+ || (command.bottomStackPosition !== undefined && !['beforeControls', 'afterControls'].includes(command.bottomStackPosition))
246
+ || (command.emptyBehavior !== undefined && !['message', 'hide'].includes(command.emptyBehavior))) {
247
+ throw new Error('Invalid detail presentation input.');
248
+ }
249
+ if (Object.keys(command).length)
250
+ next = configureTableDetailPresentation(next, command);
251
+ const issues = tableDetailAuthoringIssues(next);
252
+ if (issues.length)
253
+ throw new Error(`Invalid detail configuration: ${issues.join(', ')}`);
254
+ return next;
255
+ }
256
+ /** Explicit authoring commands only. Hydration never calls this function. */
257
+ function configureTableDetailPresentation(config, command) {
258
+ const behavior = config.behavior ?? {};
259
+ const detail = behavior.detail ?? {};
260
+ const presentation = { ...detail.presentation };
261
+ if (command.placement !== undefined)
262
+ presentation.placement = command.placement;
263
+ if (command.initiallyCollapsed !== undefined)
264
+ presentation.initiallyCollapsed = command.initiallyCollapsed;
265
+ if (command.bottomStackPosition !== undefined)
266
+ presentation.bottomStackPosition = command.bottomStackPosition;
267
+ if (command.emptyBehavior !== undefined)
268
+ presentation.emptyBehavior = command.emptyBehavior;
269
+ const next = { ...config, behavior: { ...behavior, detail: { ...detail } } };
270
+ const target = next.behavior;
271
+ if (Object.keys(presentation).length)
272
+ target.detail.presentation = presentation;
273
+ if (command.enabled !== undefined)
274
+ target.detail.enabled = command.enabled;
275
+ if (command.enabled === false) {
276
+ target.expansion = { ...behavior.expansion, enabled: false };
277
+ }
278
+ else if (command.placement !== undefined || command.enabled === true) {
279
+ target.detail.enabled = true;
280
+ const bottom = (command.placement ?? detail.presentation?.placement ?? 'row') === 'bottom';
281
+ target.expansion = { ...behavior.expansion, enabled: !bottom };
282
+ if (bottom && behavior.selection?.enabled !== true) {
283
+ target.selection = {
284
+ mode: 'both', allowSelectAll: false, checkboxPosition: 'start',
285
+ persistSelection: false, persistOnDataUpdate: false,
286
+ ...behavior.selection, type: 'single', enabled: true,
287
+ };
288
+ }
289
+ }
290
+ return next;
291
+ }
292
+ function tableDetailAuthoringIssues(config) {
293
+ const detail = config.behavior?.detail;
294
+ if (detail === undefined)
295
+ return [];
296
+ if (!isObject(detail))
297
+ return ['structure'];
298
+ for (const key of ['presentation', 'source', 'schemaContract', 'rendering', 'height', 'lazyLoad']) {
299
+ if (detail[key] !== undefined && !isObject(detail[key]))
300
+ return ['structure'];
301
+ }
302
+ const sourceMode = detail.source?.mode;
303
+ const resource = detail.source?.resource;
304
+ const resourcePath = detail.source?.resourcePath;
305
+ const resourceAllowList = detail.source?.resourceAllowList;
306
+ if ((detail.enabled !== undefined && typeof detail.enabled !== 'boolean')
307
+ || (detail.presentation?.initiallyCollapsed !== undefined && typeof detail.presentation.initiallyCollapsed !== 'boolean')
308
+ || (detail.presentation?.bottomStackPosition !== undefined && !['beforeControls', 'afterControls'].includes(detail.presentation.bottomStackPosition))
309
+ || (detail.presentation?.emptyBehavior !== undefined && !['message', 'hide'].includes(detail.presentation.emptyBehavior))
310
+ || (detail.source?.mode !== undefined && !['inline', 'resource', 'resourcePath', 'hypermedia'].includes(detail.source.mode))
311
+ || (detail.source?.inlineSchema !== undefined && !isObject(detail.source.inlineSchema))
312
+ || (detail.source?.inlineSchema?.items !== undefined && !Array.isArray(detail.source.inlineSchema.items))
313
+ || (resource !== undefined && !isObject(resource))
314
+ || (resourcePath !== undefined && !isObject(resourcePath))
315
+ || (detail.source?.contextMap !== undefined && !isObject(detail.source.contextMap))
316
+ || (resourcePath?.paramsMap !== undefined && !isObject(resourcePath.paramsMap))
317
+ || (resourceAllowList !== undefined && (!Array.isArray(resourceAllowList) || resourceAllowList.some((entry) => typeof entry !== 'string')))
318
+ || (detail.source?.fallbackMode !== undefined && !['none', 'inline', 'resource'].includes(detail.source.fallbackMode))
319
+ || (detail.lazyLoad?.enabled !== undefined && typeof detail.lazyLoad.enabled !== 'boolean')
320
+ || (detail.lazyLoad?.actionId !== undefined && typeof detail.lazyLoad.actionId !== 'string')
321
+ || (detail.lazyLoad?.cache !== undefined && !isObject(detail.lazyLoad.cache))
322
+ || (detail.lazyLoad?.cache?.enabled !== undefined && typeof detail.lazyLoad.cache.enabled !== 'boolean')
323
+ || (detail.lazyLoad?.cache?.ttlMs !== undefined && (!Number.isFinite(detail.lazyLoad.cache.ttlMs) || detail.lazyLoad.cache.ttlMs <= 0))
324
+ || (detail.lazyLoad?.retry !== undefined && !isObject(detail.lazyLoad.retry))
325
+ || (detail.lazyLoad?.retry?.maxAttempts !== undefined && (!Number.isInteger(detail.lazyLoad.retry.maxAttempts) || detail.lazyLoad.retry.maxAttempts < 0))
326
+ || (detail.lazyLoad?.cancelOnCollapse !== undefined && typeof detail.lazyLoad.cancelOnCollapse !== 'boolean')
327
+ || (detail.lazyLoad?.dedupeByRowKey !== undefined && typeof detail.lazyLoad.dedupeByRowKey !== 'boolean'))
328
+ return ['structure'];
329
+ const issues = [];
330
+ const placement = detail.presentation?.placement ?? 'row';
331
+ if (placement !== 'row' && placement !== 'bottom')
332
+ issues.push('placement');
333
+ if ((placement === 'bottom' || detail.enabled === false) && config.behavior?.expansion?.enabled)
334
+ issues.push('conflict');
335
+ if (detail.enabled !== false && placement === 'bottom' && !config.behavior?.selection?.enabled)
336
+ issues.push('selection');
337
+ if (detail.height?.px !== undefined && (!Number.isFinite(detail.height.px) || detail.height.px <= 0))
338
+ issues.push('height');
339
+ if (detail.height?.mode !== undefined && !['fixed', 'dynamic'].includes(detail.height.mode))
340
+ issues.push('height');
341
+ if (detail.height?.mode === 'fixed' && detail.height?.px === undefined)
342
+ issues.push('height');
343
+ const validResource = !!resource
344
+ && ['ui-composition', 'form-schema', 'table-schema', 'dashboard-schema'].includes(resource.kind)
345
+ && typeof resource.id === 'string' && !!resource.id.trim()
346
+ && typeof resource.version === 'string' && !!resource.version.trim();
347
+ const validInline = detail.source?.inlineSchema !== undefined;
348
+ const effectiveSourceMode = sourceMode ?? 'inline';
349
+ if (detail.enabled !== false && effectiveSourceMode === 'inline' && !validInline)
350
+ issues.push('source');
351
+ if (sourceMode === 'resource' && !validResource)
352
+ issues.push('source');
353
+ if (detail.source?.fallbackMode !== undefined
354
+ && detail.source.fallbackMode !== 'none'
355
+ && detail.source.fallbackMode === effectiveSourceMode)
356
+ issues.push('source');
357
+ if (sourceMode === 'resourcePath' && (typeof resourcePath?.path !== 'string' || !resourcePath.path.trim()
358
+ || !isTableDetailResourcePathTemplateSafe(resourcePath.path)
359
+ || (resourcePath.method !== undefined && !['GET', 'POST'].includes(resourcePath.method))
360
+ || !Array.isArray(resourceAllowList) || resourceAllowList.length === 0
361
+ || resourceAllowList.some((entry) => !entry.trim() || !isTableDetailResourcePathTemplateSafe(entry))))
362
+ issues.push('source');
363
+ if (detail.source?.fallbackMode === 'inline' && !validInline)
364
+ issues.push('source');
365
+ if (detail.source?.fallbackMode === 'resource' && !validResource)
366
+ issues.push('source');
367
+ if (validInline && (effectiveSourceMode === 'inline' || detail.source?.fallbackMode === 'inline')) {
368
+ const inlineIssues = inspectInlineDetailSchema(detail.source.inlineSchema, config);
369
+ issues.push(...inlineIssues);
370
+ }
371
+ return [...new Set(issues)];
372
+ }
373
+ function inspectInlineDetailSchema(schema, config) {
374
+ const issues = [];
375
+ const declaredActionIds = new Set((config.actions?.row?.actions ?? [])
376
+ .map((action) => String(action.id || '').trim())
377
+ .filter(Boolean));
378
+ const seen = new WeakSet();
379
+ const visit = (value) => {
380
+ if (!value || typeof value !== 'object')
381
+ return;
382
+ if (seen.has(value))
383
+ return;
384
+ seen.add(value);
385
+ if (Array.isArray(value)) {
386
+ value.forEach(visit);
387
+ return;
388
+ }
389
+ const node = value;
390
+ if (node['type'] === 'richContent') {
391
+ if (!validateRichContentDocument(node['document']).valid
392
+ || !isSafeRichContentClassName(node['rootClassName'])) {
393
+ issues.push('content');
394
+ }
395
+ }
396
+ if (typeof node['actionId'] === 'string') {
397
+ const actionId = node['actionId'].trim();
398
+ if (!actionId || !declaredActionIds.has(actionId))
399
+ issues.push('action');
400
+ }
401
+ Object.values(node).forEach(visit);
402
+ };
403
+ visit(schema);
404
+ const offContract = hasOffContractRichChildren([schema]);
405
+ if (offContract.cardGrid)
406
+ issues.push('cardGridContent');
407
+ if (offContract.compose)
408
+ issues.push('composeContent');
409
+ return issues;
410
+ }
411
+ /** Rich node types accepted as `cardGrid` card content by the canonical `TableDetailCardGridCardNode` contract. */
412
+ const TABLE_DETAIL_CARD_GRID_PRESENTER_TYPES = new Set([
413
+ 'text', 'icon', 'image', 'badge', 'avatar', 'metric', 'progress', 'progressBar',
414
+ 'microVisualization', 'link', 'chip', 'button', 'rating',
415
+ ]);
416
+ /** `RichComposeNode.items` accepts presenters only (no nested compose). */
417
+ function isTableDetailComposeItemNode(node) {
418
+ if (!node || typeof node !== 'object' || Array.isArray(node))
419
+ return false;
420
+ return TABLE_DETAIL_CARD_GRID_PRESENTER_TYPES.has(String(node['type'] || '').trim());
421
+ }
422
+ function isTableDetailCardGridContentNode(node) {
423
+ if (!node || typeof node !== 'object' || Array.isArray(node))
424
+ return false;
425
+ const type = String(node['type'] || '').trim();
426
+ return type === 'compose' || TABLE_DETAIL_CARD_GRID_PRESENTER_TYPES.has(type);
427
+ }
428
+ /**
429
+ * Projects an authored node into canonical `cardGrid` card content (`RichPresenterNode | RichComposeNode`).
430
+ * Table-detail `value` fields become `metric` (labelled) or `text` presenters; structural blocks that
431
+ * cannot live inside a rich card (stack, tabs, card, timeline, chartRef, richContent) return `null`.
432
+ */
433
+ function toTableDetailCardGridContentNode(node) {
434
+ if (isTableDetailCardGridContentNode(node))
435
+ return node;
436
+ const type = String(node?.['type'] || '').trim();
437
+ if (type === 'cardGrid') {
438
+ const firstCard = Array.isArray(node['cards']) ? node['cards'][0] : undefined;
439
+ const firstList = Array.isArray(firstCard?.content)
440
+ ? firstCard.content
441
+ : Array.isArray(firstCard?.items)
442
+ ? firstCard.items
443
+ : undefined;
444
+ const first = firstList?.[0];
445
+ return first ? toTableDetailCardGridContentNode(structuredClone(first)) : null;
446
+ }
447
+ if (type !== 'value')
448
+ return null;
449
+ const field = String(node['field'] || node['valueField'] || '').trim();
450
+ const authoredExpr = typeof node['valueExpr'] === 'string' ? node['valueExpr'].trim().replace(/^=\s*/, '') : '';
451
+ const valueExpr = authoredExpr || (field ? `row.${field}` : '');
452
+ const label = typeof node['label'] === 'string' && node['label'].trim()
453
+ ? node['label'].trim()
454
+ : typeof node['title'] === 'string' && node['title'].trim()
455
+ ? node['title'].trim()
456
+ : '';
457
+ if (valueExpr) {
458
+ return {
459
+ ...(node['id'] ? { id: node['id'] } : {}),
460
+ ...(label ? { type: 'metric', label, valueExpr } : { type: 'text', textExpr: valueExpr }),
461
+ ...(node['visibleWhen'] !== undefined && node['visibleWhen'] !== null ? { visibleWhen: node['visibleWhen'] } : {}),
462
+ };
463
+ }
464
+ if (Object.prototype.hasOwnProperty.call(node, 'value') && node['value'] !== undefined && node['value'] !== null) {
465
+ const literalText = String(node['value']);
466
+ if (label) {
467
+ return {
468
+ type: 'compose',
469
+ ...(node['id'] ? { id: node['id'] } : {}),
470
+ className: 'pfx-expansion-node pfx-expansion-node-value',
471
+ direction: 'column',
472
+ gap: 'xs',
473
+ items: [
474
+ { type: 'text', className: 'pfx-expansion-node-value__label', text: label },
475
+ { type: 'text', className: 'pfx-expansion-node-value__content', text: literalText },
476
+ ],
477
+ ...(node['visibleWhen'] !== undefined && node['visibleWhen'] !== null ? { visibleWhen: node['visibleWhen'] } : {}),
478
+ };
479
+ }
480
+ return {
481
+ type: 'text',
482
+ ...(node['id'] ? { id: node['id'] } : {}),
483
+ text: literalText,
484
+ ...(node['visibleWhen'] !== undefined && node['visibleWhen'] !== null ? { visibleWhen: node['visibleWhen'] } : {}),
485
+ };
486
+ }
487
+ return null;
488
+ }
489
+ /** Projects an authored node into a canonical compose item (presenter); a compose result yields its first presenter. */
490
+ function toTableDetailComposeItemNode(node) {
491
+ if (node &&
492
+ typeof node === 'object' &&
493
+ node['type'] === 'value' &&
494
+ !node['field'] &&
495
+ !node['valueField'] &&
496
+ !node['valueExpr'] &&
497
+ Object.prototype.hasOwnProperty.call(node, 'value') &&
498
+ node['value'] !== undefined &&
499
+ node['value'] !== null) {
500
+ const label = typeof node['label'] === 'string' && node['label'].trim()
501
+ ? node['label'].trim()
502
+ : typeof node['title'] === 'string' && node['title'].trim()
503
+ ? node['title'].trim()
504
+ : '';
505
+ return {
506
+ type: 'text',
507
+ ...(node['id'] ? { id: node['id'] } : {}),
508
+ text: label ? `${label}: ${String(node['value'])}` : String(node['value']),
509
+ ...(node['visibleWhen'] !== undefined && node['visibleWhen'] !== null ? { visibleWhen: node['visibleWhen'] } : {}),
510
+ };
511
+ }
512
+ const projected = toTableDetailCardGridContentNode(node);
513
+ if (!projected)
514
+ return null;
515
+ if (projected['type'] !== 'compose')
516
+ return projected;
517
+ const first = Array.isArray(projected['items'])
518
+ ? projected['items'].find((item) => isTableDetailComposeItemNode(item))
519
+ : undefined;
520
+ return first ? structuredClone(first) : null;
521
+ }
522
+ /** True when a cardGrid card or a compose (outside richContent documents) holds off-contract children. */
523
+ function hasOffContractRichChildren(nodes) {
524
+ const result = { cardGrid: false, compose: false };
525
+ const visit = (value) => {
526
+ if (!Array.isArray(value))
527
+ return;
528
+ for (const node of value) {
529
+ if (!node || typeof node !== 'object' || Array.isArray(node))
530
+ continue;
531
+ const record = node;
532
+ if (record['type'] === 'richContent')
533
+ continue;
534
+ if (record['type'] === 'cardGrid' && Array.isArray(record['cards'])) {
535
+ for (const card of record['cards']) {
536
+ const content = Array.isArray(card?.['content']) ? card?.['content'] : card?.['items'];
537
+ if (Array.isArray(content) && content.some((item) => !isTableDetailCardGridContentNode(item))) {
538
+ result.cardGrid = true;
539
+ }
540
+ }
541
+ }
542
+ if (record['type'] === 'compose' && Array.isArray(record['items'])
543
+ && record['items'].some((item) => !isTableDetailComposeItemNode(item))) {
544
+ result.compose = true;
545
+ }
546
+ visit(record['items']);
547
+ visit(record['content']);
548
+ visit(record['cards']);
549
+ }
550
+ };
551
+ visit(nodes);
552
+ return result;
553
+ }
554
+ /**
555
+ * Losslessly rewrites legacy table-detail `value` fields that sit inside cardGrid cards or compose items
556
+ * into canonical presenters. Structural blocks are left untouched (still reported) to avoid data loss.
557
+ * Returns the number of rewritten nodes.
558
+ */
559
+ function normalizeTableDetailRichChildren(schema) {
560
+ let rewritten = 0;
561
+ const rewrite = (list, project, accepts) => {
562
+ if (!Array.isArray(list))
563
+ return;
564
+ list.forEach((item, index) => {
565
+ if (accepts(item) || item?.['type'] !== 'value')
566
+ return;
567
+ const projected = project(item);
568
+ if (projected) {
569
+ list[index] = projected;
570
+ rewritten += 1;
571
+ }
572
+ });
573
+ };
574
+ const visit = (value) => {
575
+ if (!Array.isArray(value))
576
+ return;
577
+ for (const node of value) {
578
+ if (!node || typeof node !== 'object' || Array.isArray(node) || node['type'] === 'richContent')
579
+ continue;
580
+ if (node['type'] === 'cardGrid' && Array.isArray(node['cards'])) {
581
+ for (const card of node['cards']) {
582
+ rewrite(Array.isArray(card?.['content']) ? card?.['content'] : card?.['items'], toTableDetailCardGridContentNode, isTableDetailCardGridContentNode);
583
+ }
584
+ }
585
+ if (node['type'] === 'compose')
586
+ rewrite(node['items'], toTableDetailComposeItemNode, isTableDetailComposeItemNode);
587
+ visit(node['items']);
588
+ visit(node['content']);
589
+ visit(node['cards']);
590
+ }
591
+ };
592
+ visit([schema]);
593
+ return rewritten;
594
+ }
595
+ /** Edits the original authoring tree; keys exist only in the editor's WeakMap. */
596
+ class TableDetailTreeDraft {
597
+ keys = new WeakMap();
598
+ sequence = 0;
599
+ selectedKey = null;
600
+ schema;
601
+ constructor(schema = { layout: 'stack', items: [] }) {
602
+ const nodeType = String(schema?.type || '').trim();
603
+ if (!nodeType) {
604
+ this.schema = schema;
605
+ }
606
+ else if (nodeType === 'tabs') {
607
+ this.schema = {
608
+ ...schema,
609
+ layout: 'tabs',
610
+ items: Array.isArray(schema.items)
611
+ ? schema.items
612
+ : (Array.isArray(schema.content) ? schema.content : []),
613
+ };
614
+ }
615
+ else {
616
+ this.schema = { layout: 'stack', items: [schema] };
617
+ }
618
+ }
619
+ entries() {
620
+ const result = [];
621
+ const visit = (nodes, depth) => {
622
+ if (!Array.isArray(nodes))
623
+ return;
624
+ for (const node of nodes) {
625
+ if (!node || typeof node !== 'object' || Array.isArray(node))
626
+ continue;
627
+ let key = this.keys.get(node);
628
+ if (!key) {
629
+ key = `detail-node-${++this.sequence}`;
630
+ this.keys.set(node, key);
631
+ }
632
+ result.push({ key, node, siblings: nodes, depth });
633
+ for (const child of ['items', 'content', 'cards', 'nodes'])
634
+ visit(node[child], depth + 1);
635
+ }
636
+ };
637
+ visit(this.schema.items, 0);
638
+ return result;
639
+ }
640
+ selected() { return this.entries().find(entry => entry.key === this.selectedKey); }
641
+ move(direction) {
642
+ const entry = this.selected();
643
+ if (!entry)
644
+ return;
645
+ const index = entry.siblings.indexOf(entry.node);
646
+ const next = index + direction;
647
+ if (next < 0 || next >= entry.siblings.length)
648
+ return;
649
+ entry.siblings.splice(index, 1);
650
+ entry.siblings.splice(next, 0, entry.node);
651
+ }
652
+ add(node, nested = false) {
653
+ const selectedEntry = this.selected();
654
+ const selected = selectedEntry?.node;
655
+ const isCardLike = selected?.type === 'card' || (!selected?.type && (Array.isArray(selected?.content) || Array.isArray(selected?.items)));
656
+ // Cards stored in `cardGrid.cards` follow TableDetailCardGridCardNode: rich presenters/compose only.
657
+ const isCardGridCard = !!selectedEntry && isCardLike && this.entries().some((entry) => entry.node['type'] === 'cardGrid' && entry.node['cards'] === selectedEntry.siblings);
658
+ const childKey = selected?.type === 'cardGrid'
659
+ ? 'cards'
660
+ : (isCardLike || selected?.type === 'tab')
661
+ ? (!Array.isArray(selected?.content) && Array.isArray(selected?.items) ? 'items' : 'content')
662
+ : 'items';
663
+ const parent = nested && selected ? selected : this.schema;
664
+ const key = nested && selected ? childKey : 'items';
665
+ if (!nested && this.schema.layout === 'tabs' && node.type !== 'tab') {
666
+ node = { type: 'tab', title: node.title || node.label || node.type, content: [node] };
667
+ }
668
+ else if (nested && selected?.type === 'cardGrid') {
669
+ if (node.type === 'cardGrid' && Array.isArray(node.cards) && node.cards.length > 0) {
670
+ node = structuredClone(node.cards[0]);
671
+ }
672
+ else if (node.type === 'card') {
673
+ const rawCardChildren = Array.isArray(node.content) ? node.content : Array.isArray(node.items) ? node.items : [];
674
+ const content = rawCardChildren
675
+ .map((child) => toTableDetailCardGridContentNode(child))
676
+ .filter((child) => !!child);
677
+ node = {
678
+ ...(node.id ? { id: node.id } : {}),
679
+ title: node.title ?? '',
680
+ ...(node.subtitle !== undefined ? { subtitle: node.subtitle } : {}),
681
+ content: content.length ? content : [{ type: 'text', text: node.title || 'Detalhe' }],
682
+ };
683
+ }
684
+ else if (!Array.isArray(node.content) && !Array.isArray(node.items)) {
685
+ const content = toTableDetailCardGridContentNode(node);
686
+ if (!content)
687
+ return null;
688
+ node = {
689
+ title: node.title || node.label || '',
690
+ content: [content],
691
+ };
692
+ }
693
+ }
694
+ else if (nested && isCardGridCard) {
695
+ const content = toTableDetailCardGridContentNode(node);
696
+ if (!content)
697
+ return null;
698
+ node = content;
699
+ }
700
+ else if (nested && selected?.type === 'compose') {
701
+ // RichComposeNode.items accepts presenters only; the runtime drops anything else silently.
702
+ const item = toTableDetailComposeItemNode(node);
703
+ if (!item)
704
+ return null;
705
+ node = item;
706
+ }
707
+ else if (nested && isCardLike && node.type === 'cardGrid' && Array.isArray(node.cards)) {
708
+ const firstCardContent = node.cards[0]?.content ?? node.cards[0]?.items;
709
+ if (Array.isArray(firstCardContent) && firstCardContent.length > 0) {
710
+ node = structuredClone(firstCardContent[0]);
711
+ }
712
+ }
713
+ if (!Array.isArray(parent[key]))
714
+ parent[key] = [];
715
+ parent[key].push(node);
716
+ this.selectedKey = this.entries().find(entry => entry.node === node)?.key ?? null;
717
+ return node;
718
+ }
719
+ }
720
+
721
+ const DOCUMENT_KIND = 'praxis.table.editor';
722
+ const DOCUMENT_VERSION = 1;
723
+ const HORIZONTAL_SCROLL_VALUES = ['auto', 'wrap', 'none'];
724
+ function createTableAuthoringDocument(source) {
725
+ const doc = {
726
+ kind: DOCUMENT_KIND,
727
+ version: DOCUMENT_VERSION,
728
+ config: asTableConfig(source?.config),
729
+ };
730
+ if (hasOwnProperty(source, 'bindings')) {
731
+ doc.bindings = source?.bindings;
732
+ }
733
+ return normalizeTableAuthoringDocument(doc);
734
+ }
735
+ function parseLegacyOrTableDocument(raw) {
736
+ const obj = asRecord(raw);
737
+ if (obj.kind === DOCUMENT_KIND && obj.version === DOCUMENT_VERSION) {
738
+ return normalizeTableAuthoringDocument({
739
+ kind: DOCUMENT_KIND,
740
+ version: DOCUMENT_VERSION,
741
+ config: asTableConfig(obj.config),
742
+ bindings: obj.bindings,
743
+ });
744
+ }
745
+ if (looksLikeLegacyPayload(obj)) {
746
+ const legacyConfig = extractLegacyTableConfig(obj);
747
+ const legacyIdField = normalizeIdField(obj.__idField__);
748
+ if (legacyIdField) {
749
+ legacyConfig.meta = {
750
+ ...(legacyConfig.meta || {}),
751
+ idField: legacyIdField,
752
+ };
753
+ }
754
+ return normalizeTableAuthoringDocument({
755
+ kind: DOCUMENT_KIND,
756
+ version: DOCUMENT_VERSION,
757
+ config: legacyConfig,
758
+ bindings: {
759
+ resourcePath: normalizeResourcePath(obj.__resourcePath__),
760
+ horizontalScroll: normalizeHorizontalScroll(obj.__horizontalScroll__),
761
+ },
762
+ });
763
+ }
764
+ return createTableAuthoringDocument({ config: raw });
765
+ }
766
+ function normalizeTableAuthoringDocument(doc) {
767
+ const config = ensureAuthoringCompleteness(normalizeTableConfig(doc?.config));
768
+ const legacyBindingIdField = normalizeIdField(doc?.bindings?.idField);
769
+ const legacyRuntimeIdField = normalizeIdField(doc?.config?.__idField__);
770
+ const canonicalIdField = normalizeIdField(config?.meta?.idField);
771
+ const migratedIdField = canonicalIdField || legacyBindingIdField || legacyRuntimeIdField;
772
+ if (migratedIdField && !canonicalIdField) {
773
+ config.meta = {
774
+ ...(config.meta || {}),
775
+ idField: migratedIdField,
776
+ };
777
+ }
778
+ const normalized = {
779
+ kind: DOCUMENT_KIND,
780
+ version: DOCUMENT_VERSION,
781
+ config,
782
+ };
783
+ if (hasOwnProperty(doc, 'bindings')) {
784
+ normalized.bindings = normalizeBindings(doc?.bindings);
785
+ }
786
+ return normalized;
787
+ }
788
+ function validateTableAuthoringDocument(doc, context) {
789
+ const raw = asRecord(doc);
790
+ const rawConfig = asRecord(raw.config);
791
+ const rawBindings = asRecord(raw.bindings);
792
+ const normalized = normalizeTableAuthoringDocument(doc);
793
+ const diagnostics = [];
794
+ projectTableActionAuthoring(rawConfig, undefined, diagnostic => diagnostics.push(diagnostic));
795
+ for (const issue of tableDetailAuthoringIssues(rawConfig)) {
796
+ diagnostics.push(errorDiagnostic(`table.config.detail.${issue}`, `Invalid detail configuration: ${issue}`, 'config.behavior.detail'));
797
+ }
798
+ if ('columns' in rawConfig && !Array.isArray(rawConfig.columns)) {
799
+ diagnostics.push(errorDiagnostic('table.config.columns.invalid', 'config.columns must be an array', 'config.columns'));
800
+ return diagnostics;
801
+ }
802
+ const rawColumns = Array.isArray(rawConfig.columns)
803
+ ? rawConfig.columns
804
+ : (normalized.config.columns || []);
805
+ const columnFields = new Set();
806
+ rawColumns.forEach((column, index) => {
807
+ const field = trimString(column?.field);
808
+ if (!field) {
809
+ diagnostics.push(errorDiagnostic('table.config.columns.field.required', 'Column field is required', `config.columns[${index}].field`));
810
+ }
811
+ else if (columnFields.has(field)) {
812
+ diagnostics.push(errorDiagnostic('table.config.columns.field.duplicate', 'Column field must be unique within the table', `config.columns[${index}].field`));
813
+ }
814
+ else {
815
+ columnFields.add(field);
816
+ }
817
+ });
818
+ if ('columnProjection' in rawConfig) {
819
+ const projection = asRecord(rawConfig.columnProjection);
820
+ if (!rawConfig.columnProjection
821
+ || typeof rawConfig.columnProjection !== 'object'
822
+ || Array.isArray(rawConfig.columnProjection)) {
823
+ diagnostics.push(errorDiagnostic('table.config.columnProjection.invalid', 'config.columnProjection must be an object', 'config.columnProjection'));
824
+ }
825
+ else if (projection.source !== 'schema') {
826
+ diagnostics.push(errorDiagnostic('table.config.columnProjection.source.invalid', 'config.columnProjection.source must be schema', 'config.columnProjection.source'));
827
+ }
828
+ if ('include' in projection) {
829
+ if (!Array.isArray(projection.include)
830
+ || projection.include.some((field) => typeof field !== 'string' || !field.trim())
831
+ || new Set(projection.include).size !== projection.include.length) {
832
+ diagnostics.push(errorDiagnostic('table.config.columnProjection.include.invalid', 'config.columnProjection.include must be an ordered array of unique canonical field names', 'config.columnProjection.include'));
833
+ }
834
+ }
835
+ if ('order' in projection && (!Array.isArray(projection.order)
836
+ || projection.order.some((field) => typeof field !== 'string' || !field.trim() || field !== field.trim())
837
+ || new Set(projection.order).size !== projection.order.length)) {
838
+ diagnostics.push(errorDiagnostic('table.config.columnProjection.order.invalid', 'config.columnProjection.order must contain unique canonical field names without selecting membership', 'config.columnProjection.order'));
839
+ }
840
+ if ('overrides' in projection) {
841
+ const overrides = asRecord(projection.overrides);
842
+ if (!projection.overrides
843
+ || typeof projection.overrides !== 'object'
844
+ || Array.isArray(projection.overrides)) {
845
+ diagnostics.push(errorDiagnostic('table.config.columnProjection.overrides.invalid', 'config.columnProjection.overrides must be an object keyed by canonical field name', 'config.columnProjection.overrides'));
846
+ }
847
+ else {
848
+ Object.entries(overrides).forEach(([field, override]) => {
849
+ if (!field.trim() || !override || typeof override !== 'object' || Array.isArray(override)) {
850
+ diagnostics.push(errorDiagnostic('table.config.columnProjection.override.invalid', 'Each column projection override must be an object keyed by canonical field name', `config.columnProjection.overrides.${field}`));
851
+ return;
852
+ }
853
+ if (hasOwnProperty(override, 'field')) {
854
+ diagnostics.push(errorDiagnostic('table.config.columnProjection.override.field.forbidden', 'A schema column projection override cannot change the canonical field identity', `config.columnProjection.overrides.${field}.field`));
855
+ }
856
+ });
857
+ }
858
+ }
859
+ if ('additions' in projection) {
860
+ if (!Array.isArray(projection.additions)) {
861
+ diagnostics.push(errorDiagnostic('table.config.columnProjection.additions.invalid', 'config.columnProjection.additions must be an array of editorial columns', 'config.columnProjection.additions'));
862
+ }
863
+ else {
864
+ const fields = projection.additions
865
+ .map((column) => trimString(asRecord(column).field));
866
+ if (fields.some((field) => !field)
867
+ || new Set(fields).size !== fields.length) {
868
+ diagnostics.push(errorDiagnostic('table.config.columnProjection.additions.field.invalid', 'Each editorial addition must have a unique field', 'config.columnProjection.additions'));
869
+ }
870
+ }
871
+ }
872
+ }
873
+ const bindings = normalized.bindings || {};
874
+ const metaIdField = trimString(normalized.config?.meta?.idField);
875
+ if ('horizontalScroll' in rawBindings &&
876
+ rawBindings.horizontalScroll !== undefined &&
877
+ normalizeHorizontalScroll(rawBindings.horizontalScroll) === undefined) {
878
+ diagnostics.push(errorDiagnostic('table.bindings.horizontalScroll.invalid', 'horizontalScroll must be auto, wrap or none', 'bindings.horizontalScroll'));
879
+ }
880
+ if (bindings.horizontalScroll &&
881
+ !HORIZONTAL_SCROLL_VALUES.includes(bindings.horizontalScroll)) {
882
+ diagnostics.push(errorDiagnostic('table.bindings.horizontalScroll.invalid', 'horizontalScroll must be auto, wrap or none', 'bindings.horizontalScroll'));
883
+ }
884
+ const effectiveMode = resolveEffectiveDataMode(normalized, context);
885
+ if (rawConfig.columnProjection && effectiveMode !== 'remote') {
886
+ diagnostics.push(warnDiagnostic('table.config.columnProjection.remote-schema.required', 'Schema column projection requires a remote resourcePath at runtime', 'config.columnProjection'));
887
+ }
888
+ if (effectiveMode === 'local') {
889
+ if (normalized.config.behavior?.pagination?.strategy === 'server') {
890
+ diagnostics.push(warnDiagnostic('table.behavior.pagination.strategy.invalid-for-local', 'Server pagination will be projected to client in local mode', 'config.behavior.pagination.strategy'));
891
+ }
892
+ if (normalized.config.behavior?.sorting?.strategy === 'server') {
893
+ diagnostics.push(warnDiagnostic('table.behavior.sorting.strategy.invalid-for-local', 'Server sorting will be projected to client in local mode', 'config.behavior.sorting.strategy'));
894
+ }
895
+ }
896
+ if (context?.server?.idField &&
897
+ metaIdField &&
898
+ context.server.idField !== metaIdField) {
899
+ diagnostics.push(warnDiagnostic('table.meta.idField.diverges-from-server', 'config.meta.idField diverges from current server idField', 'config.meta.idField'));
900
+ }
901
+ const configHash = trimString(normalized.config?.meta?.serverHash);
902
+ if (context?.server?.schemaHash && !configHash) {
903
+ diagnostics.push(infoDiagnostic('table.meta.serverHash.snapshot-missing', 'No persisted schema snapshot is available for reconciliation with the current server schema hash', 'config.meta.serverHash'));
904
+ }
905
+ if (context?.server?.schemaHash && configHash && context.server.schemaHash !== configHash) {
906
+ diagnostics.push(warnDiagnostic('table.meta.serverHash.diverges-from-server', 'config.meta.serverHash snapshot diverges from current server schema hash', 'config.meta.serverHash'));
907
+ }
908
+ return diagnostics;
909
+ }
910
+ function toCanonicalTableConfig(doc, context) {
911
+ const normalized = normalizeTableAuthoringDocument(doc);
912
+ const cfg = cloneJson(normalized.config);
913
+ const effectiveMode = resolveEffectiveDataMode(normalized, context);
914
+ if (effectiveMode === 'local') {
915
+ if (cfg.behavior?.pagination?.strategy === 'server') {
916
+ cfg.behavior = {
917
+ ...(cfg.behavior || {}),
918
+ pagination: {
919
+ ...(cfg.behavior?.pagination || {}),
920
+ strategy: 'client',
921
+ },
922
+ };
923
+ }
924
+ if (cfg.behavior?.sorting?.strategy === 'server') {
925
+ cfg.behavior = {
926
+ ...(cfg.behavior || {}),
927
+ sorting: {
928
+ ...(cfg.behavior?.sorting || {}),
929
+ strategy: 'client',
930
+ },
931
+ };
932
+ }
933
+ }
934
+ return stripLegacyRuntimeMarkers(cfg);
935
+ }
936
+ function buildTableApplyPlan(doc, runtime, options) {
937
+ const normalized = normalizeTableAuthoringDocument(doc);
938
+ const hasBindings = hasOwnProperty(doc, 'bindings');
939
+ const diagnostics = validateTableAuthoringDocument(doc, {
940
+ hasLocalDataInput: runtime?.hasLocalDataInput,
941
+ localDataModeDefaultEnabled: runtime?.localDataModeDefaultEnabled,
942
+ server: {
943
+ idField: runtime?.server?.idField,
944
+ schemaHash: runtime?.server?.schemaHash,
945
+ },
946
+ });
947
+ const bindingsPatch = hasBindings
948
+ ? normalizeBindings(normalized.bindings) || {}
949
+ : undefined;
950
+ const diff = deriveBindingsDiff(bindingsPatch, runtime?.currentBindings);
951
+ return {
952
+ canonicalConfig: toCanonicalTableConfig(normalized, {
953
+ hasLocalDataInput: runtime?.hasLocalDataInput,
954
+ localDataModeDefaultEnabled: runtime?.localDataModeDefaultEnabled,
955
+ }),
956
+ bindingsPatch,
957
+ persistence: {
958
+ saveConfig: options?.saveConfig === true,
959
+ saveBindings: options?.saveBindings === true,
960
+ },
961
+ runtime: deriveRuntimePlan(bindingsPatch, diff),
962
+ metadata: {
963
+ attachServerMeta: options?.attachServerMeta === true,
964
+ },
965
+ diff,
966
+ diagnostics,
967
+ };
968
+ }
969
+ function serializeTableAuthoringDocument(doc) {
970
+ const normalized = normalizeTableAuthoringDocument(doc);
971
+ return stripUndefinedDeep({
972
+ kind: DOCUMENT_KIND,
973
+ version: DOCUMENT_VERSION,
974
+ config: normalized.config,
975
+ bindings: normalized.bindings,
976
+ });
977
+ }
978
+ function normalizeTableConfig(config) {
979
+ const normalized = cloneJson(projectTableActionAuthoring(config || { columns: [] }));
980
+ if (!Array.isArray(normalized.columns)) {
981
+ normalized.columns = [];
982
+ }
983
+ if (normalized.behavior && typeof normalized.behavior === 'object') {
984
+ Object.keys(normalized.behavior).forEach((key) => {
985
+ const value = normalized.behavior[key];
986
+ if (value &&
987
+ typeof value === 'object' &&
988
+ !Array.isArray(value) &&
989
+ Object.keys(value).length === 0) {
990
+ delete normalized.behavior[key];
991
+ }
992
+ });
993
+ if (Object.keys(normalized.behavior).length === 0) {
994
+ delete normalized.behavior;
995
+ }
996
+ }
997
+ const settings = normalized.behavior?.filtering?.advancedFilters?.settings;
998
+ if (settings && typeof settings === 'object' && !Array.isArray(settings)) {
999
+ Object.keys(settings).forEach((key) => {
1000
+ if (settings[key] === undefined || settings[key] === null || settings[key] === '') {
1001
+ delete settings[key];
1002
+ }
1003
+ });
1004
+ }
1005
+ return normalized;
1006
+ }
1007
+ function ensureAuthoringCompleteness(config) {
1008
+ const next = cloneJson(config);
1009
+ const advanced = next.behavior?.filtering?.advancedFilters;
1010
+ if (advanced &&
1011
+ typeof advanced === 'object' &&
1012
+ !Object.prototype.hasOwnProperty.call(advanced, 'settings')) {
1013
+ advanced.settings = {};
1014
+ }
1015
+ return next;
1016
+ }
1017
+ function normalizeBindings(bindings) {
1018
+ if (!bindings || typeof bindings !== 'object')
1019
+ return undefined;
1020
+ const normalized = {
1021
+ resourcePath: normalizeResourcePath(bindings.resourcePath),
1022
+ horizontalScroll: normalizeHorizontalScroll(bindings.horizontalScroll),
1023
+ };
1024
+ return stripUndefinedShallow(normalized);
1025
+ }
1026
+ function resolveEffectiveDataMode(doc, context) {
1027
+ const resourcePath = trimString(doc.bindings?.resourcePath);
1028
+ if (resourcePath)
1029
+ return 'remote';
1030
+ const localModeFlag = readBoolean(doc.config?.behavior?.localDataMode?.enabled);
1031
+ const effectiveLocalFlag = localModeFlag ?? (context?.localDataModeDefaultEnabled === true);
1032
+ if (context?.hasLocalDataInput && effectiveLocalFlag) {
1033
+ return 'local';
1034
+ }
1035
+ return 'empty';
1036
+ }
1037
+ function deriveBindingsDiff(next, current) {
1038
+ if (next === undefined) {
1039
+ return {
1040
+ resourcePathChanged: false,
1041
+ horizontalScrollChanged: false,
1042
+ requiresDataRefresh: false,
1043
+ };
1044
+ }
1045
+ const normalizedNext = normalizeBindings(next) || {};
1046
+ const normalizedCurrent = normalizeBindings(current) || {};
1047
+ const resourcePathChanged = normalizeResourcePath(normalizedNext.resourcePath) !==
1048
+ normalizeResourcePath(normalizedCurrent.resourcePath);
1049
+ const horizontalScrollChanged = normalizeHorizontalScroll(normalizedNext.horizontalScroll) !==
1050
+ normalizeHorizontalScroll(normalizedCurrent.horizontalScroll);
1051
+ return {
1052
+ resourcePathChanged,
1053
+ horizontalScrollChanged,
1054
+ requiresDataRefresh: resourcePathChanged,
1055
+ };
1056
+ }
1057
+ function deriveRuntimePlan(bindings, diff) {
1058
+ if (bindings === undefined) {
1059
+ return {
1060
+ reconnectDataSource: false,
1061
+ reloadSchema: false,
1062
+ fetchData: false,
1063
+ refreshLocalView: false,
1064
+ };
1065
+ }
1066
+ const resourcePath = trimString(bindings?.resourcePath);
1067
+ if (resourcePath) {
1068
+ return {
1069
+ reconnectDataSource: !!diff?.resourcePathChanged,
1070
+ reloadSchema: !!diff?.resourcePathChanged,
1071
+ fetchData: !!diff?.requiresDataRefresh,
1072
+ refreshLocalView: false,
1073
+ };
1074
+ }
1075
+ return {
1076
+ reconnectDataSource: !!diff?.resourcePathChanged,
1077
+ reloadSchema: false,
1078
+ fetchData: false,
1079
+ refreshLocalView: true,
1080
+ };
1081
+ }
1082
+ function extractLegacyTableConfig(obj) {
1083
+ const clone = { ...obj };
1084
+ delete clone.__resourcePath__;
1085
+ delete clone.__resourcePathIntent__;
1086
+ delete clone.__idField__;
1087
+ delete clone.__horizontalScroll__;
1088
+ return asTableConfig(clone);
1089
+ }
1090
+ function stripLegacyRuntimeMarkers(config) {
1091
+ const clone = cloneJson(config);
1092
+ delete clone.__resourcePath__;
1093
+ delete clone.__resourcePathIntent__;
1094
+ delete clone.__idField__;
1095
+ delete clone.__horizontalScroll__;
1096
+ return clone;
1097
+ }
1098
+ function looksLikeLegacyPayload(obj) {
1099
+ return ('__resourcePath__' in obj ||
1100
+ '__resourcePathIntent__' in obj ||
1101
+ '__idField__' in obj ||
1102
+ '__horizontalScroll__' in obj);
1103
+ }
1104
+ function asTableConfig(value) {
1105
+ if (value && typeof value === 'object') {
1106
+ return value;
1107
+ }
1108
+ return { columns: [] };
1109
+ }
1110
+ function asRecord(value) {
1111
+ return value && typeof value === 'object'
1112
+ ? value
1113
+ : {};
1114
+ }
1115
+ function hasOwnProperty(value, key) {
1116
+ return !!value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key);
1117
+ }
1118
+ function normalizeResourcePath(value) {
1119
+ if (value === undefined)
1120
+ return undefined;
1121
+ const trimmed = trimString(value);
1122
+ return trimmed || null;
1123
+ }
1124
+ function normalizeIdField(value) {
1125
+ const trimmed = trimString(value);
1126
+ return trimmed || undefined;
1127
+ }
1128
+ function normalizeHorizontalScroll(value) {
1129
+ const candidate = trimString(value);
1130
+ if (!candidate)
1131
+ return undefined;
1132
+ return HORIZONTAL_SCROLL_VALUES.includes(candidate) ? candidate : undefined;
1133
+ }
1134
+ function trimString(value) {
1135
+ return typeof value === 'string' ? value.trim() : '';
1136
+ }
1137
+ function readBoolean(value) {
1138
+ if (value === true || value === false)
1139
+ return value;
1140
+ return undefined;
1141
+ }
1142
+ function cloneJson(value) {
1143
+ return JSON.parse(JSON.stringify(value ?? null));
1144
+ }
1145
+ function stripUndefinedShallow(value) {
1146
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined);
1147
+ if (!entries.length)
1148
+ return undefined;
1149
+ return Object.fromEntries(entries);
1150
+ }
1151
+ function stripUndefinedDeep(value) {
1152
+ return JSON.parse(JSON.stringify(value));
1153
+ }
1154
+ function errorDiagnostic(code, message, path) {
1155
+ return { level: 'error', code, message, path };
1156
+ }
1157
+ function warnDiagnostic(code, message, path) {
1158
+ return { level: 'warning', code, message, path };
1159
+ }
1160
+ function infoDiagnostic(code, message, path) {
1161
+ return { level: 'info', code, message, path };
1162
+ }
1163
+
1164
+ /**
1165
+ * Internal canonical schema-to-column projection shared by runtime and authoring.
1166
+ * Receives already-admitted fields and intent; does not normalize untrusted schema,
1167
+ * recover historical evidence, execute user actions or attest provenance.
1168
+ * No component instance, dependency injection or network request is required.
1169
+ * Not part of the root public API.
1170
+ */
1171
+ class TableSchemaColumnProjector {
1172
+ convertFieldToColumn(field) {
1173
+ const apiType = resolveColumnTypeFromFieldDefinition(field, this.inferFieldTypeFromFieldName(field.name));
1174
+ const col = {
1175
+ field: field.name,
1176
+ header: field.label ?? field.name,
1177
+ order: field.order,
1178
+ width: field.width ?? undefined,
1179
+ sortable: field.sortable,
1180
+ visible: field.tableHidden || field.hidden ? false : true,
1181
+ type: apiType,
1182
+ _originalApiType: apiType,
1183
+ _isApiField: true,
1184
+ };
1185
+ this.applySchemaFieldPresentation(field, col);
1186
+ // Auto-renderers: map known UI controls to richer cell renderers on first bootstrap
1187
+ // - Avatar: when backend marks controlType 'avatar' (x-ui) or field name matches avatar-like patterns
1188
+ try {
1189
+ this.applyAutoRenderer(field, col);
1190
+ }
1191
+ catch { }
1192
+ return col;
1193
+ }
1194
+ projectSchemaColumns(fields, projection) {
1195
+ const overrides = projection?.overrides ?? {};
1196
+ const include = Array.isArray(projection?.include)
1197
+ ? projection.include
1198
+ .map((field) => String(field || '').trim())
1199
+ .filter((field, index, values) => !!field && values.indexOf(field) === index)
1200
+ : [];
1201
+ const includeOrder = new Map(include.map((field, index) => [field, index]));
1202
+ const order = Array.isArray(projection?.order) ? projection.order : include;
1203
+ const presentationOrder = new Map(order.map((field, index) => [field, index]));
1204
+ const schemaColumns = (Array.isArray(fields) ? fields : [])
1205
+ .filter((field) => !field.tableHidden && !field.hidden)
1206
+ .filter((field) => include.length === 0 || includeOrder.has(field.name))
1207
+ .sort((left, right) => {
1208
+ return (presentationOrder.get(left.name) ?? Number.MAX_SAFE_INTEGER)
1209
+ - (presentationOrder.get(right.name) ?? Number.MAX_SAFE_INTEGER);
1210
+ })
1211
+ .map((field) => {
1212
+ const base = this.convertFieldToColumn(field);
1213
+ const override = overrides[field.name];
1214
+ if (!override || typeof override !== 'object' || Array.isArray(override)) {
1215
+ return base;
1216
+ }
1217
+ const merged = deepMerge(base, override);
1218
+ return {
1219
+ ...merged,
1220
+ field: base.field,
1221
+ _isApiField: base._isApiField,
1222
+ _originalApiType: base._originalApiType,
1223
+ };
1224
+ });
1225
+ const canonicalFields = new Set((Array.isArray(fields) ? fields : [])
1226
+ .map((field) => String(field?.name || '').trim())
1227
+ .filter(Boolean));
1228
+ const additions = Array.isArray(projection?.additions)
1229
+ ? projection.additions
1230
+ .filter((column) => {
1231
+ const field = String(column?.field || '').trim();
1232
+ return !!field && !canonicalFields.has(field);
1233
+ })
1234
+ .map((column) => this.toPortableColumnDefinition(column))
1235
+ : [];
1236
+ // Presentation priority must also survive setupColumns' numeric ordering.
1237
+ // It never admits hidden fields, overrides field identity, or selects membership.
1238
+ const combined = [...schemaColumns, ...additions];
1239
+ if (Array.isArray(projection?.order) && projection.order.length > 0) {
1240
+ const additionFields = new Set(additions.map((col) => col.field));
1241
+ combined.sort((left, right) => {
1242
+ return (presentationOrder.get(left.field) ?? Number.MAX_SAFE_INTEGER)
1243
+ - (presentationOrder.get(right.field) ?? Number.MAX_SAFE_INTEGER);
1244
+ });
1245
+ return combined.map((column, index) => {
1246
+ if (!additionFields.has(column.field) || presentationOrder.has(column.field)) {
1247
+ return { ...column, order: index };
1248
+ }
1249
+ return column;
1250
+ });
1251
+ }
1252
+ return combined;
1253
+ }
1254
+ inferFieldTypeFromFieldName(fieldName) {
1255
+ const lowercaseName = fieldName.toLowerCase();
1256
+ // Date/time patterns - more specific patterns first (EN + PT-BR)
1257
+ if (lowercaseName.endsWith('date') ||
1258
+ lowercaseName.endsWith('time') ||
1259
+ lowercaseName.endsWith('at') ||
1260
+ lowercaseName.startsWith('date') ||
1261
+ lowercaseName.startsWith('data') ||
1262
+ lowercaseName.startsWith('dt_') ||
1263
+ lowercaseName.startsWith('dt') ||
1264
+ lowercaseName.endsWith('_em') ||
1265
+ lowercaseName === 'created' ||
1266
+ lowercaseName === 'updated' ||
1267
+ lowercaseName === 'modified' ||
1268
+ lowercaseName === 'nascimento' ||
1269
+ lowercaseName === 'admissao' ||
1270
+ lowercaseName === 'demissao' ||
1271
+ lowercaseName === 'vencimento' ||
1272
+ lowercaseName.includes('timestamp')) {
1273
+ return 'date';
1274
+ }
1275
+ // Boolean patterns - most specific first to avoid conflicts (EN + PT-BR)
1276
+ if (lowercaseName.startsWith('is_') ||
1277
+ lowercaseName.startsWith('has_') ||
1278
+ lowercaseName.startsWith('can_') ||
1279
+ lowercaseName.startsWith('should_') ||
1280
+ lowercaseName.startsWith('fl_') ||
1281
+ lowercaseName.startsWith('ind_') ||
1282
+ lowercaseName === 'active' ||
1283
+ lowercaseName === 'ativo' ||
1284
+ lowercaseName === 'inativo' ||
1285
+ lowercaseName === 'enabled' ||
1286
+ lowercaseName === 'habilitado' ||
1287
+ lowercaseName === 'visible' ||
1288
+ lowercaseName === 'visivel' ||
1289
+ lowercaseName === 'deleted' ||
1290
+ lowercaseName === 'excluido' ||
1291
+ lowercaseName === 'archived' ||
1292
+ lowercaseName === 'arquivado' ||
1293
+ lowercaseName.endsWith('_flag') ||
1294
+ lowercaseName.endsWith('_enabled') ||
1295
+ lowercaseName.endsWith('_active') ||
1296
+ lowercaseName.endsWith('_ativo')) {
1297
+ return 'boolean';
1298
+ }
1299
+ // Currency/money patterns - exclude common false positives (EN + PT-BR)
1300
+ if ((lowercaseName.includes('price') ||
1301
+ lowercaseName.includes('preco') ||
1302
+ lowercaseName.includes('amount') ||
1303
+ lowercaseName.includes('cost') ||
1304
+ lowercaseName.includes('custo') ||
1305
+ lowercaseName.includes('salary') ||
1306
+ lowercaseName.includes('salario') ||
1307
+ lowercaseName.includes('remuneracao') ||
1308
+ lowercaseName.includes('orcamento') ||
1309
+ lowercaseName.includes('indenizacao') ||
1310
+ lowercaseName.includes('multa') ||
1311
+ lowercaseName.includes('desconto') ||
1312
+ lowercaseName.includes('liquido') ||
1313
+ lowercaseName.includes('bruto') ||
1314
+ lowercaseName.includes('wage') ||
1315
+ lowercaseName.includes('fee') ||
1316
+ ((lowercaseName.includes('value') || lowercaseName.includes('valor') || lowercaseName.startsWith('vl_')) &&
1317
+ !lowercaseName.includes('id') &&
1318
+ !lowercaseName.includes('key'))) &&
1319
+ !lowercaseName.includes('count') &&
1320
+ !lowercaseName.includes('type') &&
1321
+ !lowercaseName.includes('tipo')) {
1322
+ return 'currency';
1323
+ }
1324
+ // Percentage patterns - be more specific (EN + PT-BR)
1325
+ if (lowercaseName.includes('percent') ||
1326
+ lowercaseName.includes('percentual') ||
1327
+ lowercaseName.includes('aliquota') ||
1328
+ lowercaseName.includes('margem') ||
1329
+ lowercaseName.startsWith('pct_') ||
1330
+ lowercaseName.endsWith('_rate') ||
1331
+ lowercaseName.endsWith('_ratio') ||
1332
+ lowercaseName.endsWith('_pct') ||
1333
+ lowercaseName.endsWith('_taxa') ||
1334
+ ((lowercaseName.includes('rate') || lowercaseName.includes('taxa')) &&
1335
+ !lowercaseName.includes('created') &&
1336
+ !lowercaseName.includes('updated')) ||
1337
+ lowercaseName.endsWith('_score')) {
1338
+ return 'percentage';
1339
+ }
1340
+ // Number patterns - exclude common false positives like string IDs (EN + PT-BR)
1341
+ if ((lowercaseName.endsWith('_id') && lowercaseName !== 'id') || // composite IDs might be strings
1342
+ lowercaseName === 'id' ||
1343
+ lowercaseName.includes('count') ||
1344
+ lowercaseName.includes('quantity') ||
1345
+ lowercaseName.includes('quantidade') ||
1346
+ lowercaseName.includes('qtd') ||
1347
+ lowercaseName.includes('number') ||
1348
+ lowercaseName.includes('numero') ||
1349
+ lowercaseName.includes('total') ||
1350
+ lowercaseName.includes('sum') ||
1351
+ lowercaseName.includes('soma') ||
1352
+ lowercaseName.includes('age') ||
1353
+ lowercaseName.includes('idade') ||
1354
+ lowercaseName.includes('weight') ||
1355
+ lowercaseName.includes('peso') ||
1356
+ lowercaseName.includes('height') ||
1357
+ lowercaseName.includes('altura') ||
1358
+ lowercaseName.includes('size') ||
1359
+ lowercaseName.includes('tamanho') ||
1360
+ lowercaseName.includes('pontuacao') ||
1361
+ lowercaseName.includes('score') ||
1362
+ lowercaseName.includes('nota') ||
1363
+ lowercaseName.endsWith('_num') ||
1364
+ lowercaseName.endsWith('_number')) {
1365
+ return 'number';
1366
+ }
1367
+ // Default to string for ambiguous cases
1368
+ return 'string';
1369
+ }
1370
+ applySchemaFieldPresentation(field, col) {
1371
+ if (!field || !col)
1372
+ return;
1373
+ const controlType = this.normalizePresentationToken(field.controlType);
1374
+ const numericFormat = this.normalizePresentationToken(field.numericFormat ?? field.format);
1375
+ const valuePresentationType = this.normalizePresentationToken(field.valuePresentation?.type);
1376
+ const displayFormat = this.firstNonEmptyString(field.displayFormat, field.extraProperties?.displayFormat);
1377
+ const valueMapping = this.resolveSchemaValueMapping(field);
1378
+ if (valueMapping && (!col.valueMapping || Object.keys(col.valueMapping).length === 0)) {
1379
+ col.valueMapping = valueMapping;
1380
+ }
1381
+ const presentation = field.presentation;
1382
+ if (presentation?.presenter === 'microVisualization' &&
1383
+ isPraxisPresentationVisualizationTableSafe(presentation?.visualization) &&
1384
+ !col.renderer?.type) {
1385
+ col.renderer = {
1386
+ ...(col.renderer || {}),
1387
+ type: 'microVisualization',
1388
+ microVisualization: {
1389
+ visualization: presentation.visualization,
1390
+ },
1391
+ };
1392
+ }
1393
+ this.applySchemaSemanticCellPresentation(field, col, presentation);
1394
+ if (numericFormat === 'currency' || controlType === 'currency' || col.type === 'currency') {
1395
+ const wasCurrency = col.type === 'currency';
1396
+ col.type = 'currency';
1397
+ if (!col.format || (!wasCurrency && !String(col.format).includes('|'))) {
1398
+ col.format = this.resolveSchemaCurrencyFormat(field);
1399
+ }
1400
+ return;
1401
+ }
1402
+ if (numericFormat === 'date' ||
1403
+ numericFormat === 'datetime' ||
1404
+ valuePresentationType === 'date' ||
1405
+ valuePresentationType === 'datetime' ||
1406
+ controlType === 'datepicker' ||
1407
+ controlType === 'datetimepicker' ||
1408
+ controlType === 'datetimelocal' ||
1409
+ controlType === 'date' ||
1410
+ controlType === 'datetime') {
1411
+ col.type = 'date';
1412
+ if (!col.format) {
1413
+ col.format = displayFormat || (numericFormat === 'datetime' ||
1414
+ valuePresentationType === 'datetime' ||
1415
+ controlType === 'datetimepicker' ||
1416
+ controlType === 'datetimelocal' ||
1417
+ controlType === 'datetime'
1418
+ ? 'dd/MM/yyyy HH:mm'
1419
+ : 'dd/MM/yyyy');
1420
+ }
1421
+ return;
1422
+ }
1423
+ if (col.type === 'boolean' && !col.format) {
1424
+ col.format = this.resolveSchemaBooleanFormat(field);
1425
+ }
1426
+ const schemaDataType = this.normalizePresentationToken(field.type);
1427
+ const schemaFormat = this.normalizePresentationToken(field.format);
1428
+ if (col.type === 'number' && !col.format) {
1429
+ if (displayFormat) {
1430
+ col.format = displayFormat;
1431
+ }
1432
+ else if (schemaDataType === 'integer' ||
1433
+ schemaFormat === 'integer' ||
1434
+ schemaFormat === 'int32' ||
1435
+ schemaFormat === 'int64' ||
1436
+ numericFormat === 'integer' ||
1437
+ numericFormat === 'int32' ||
1438
+ numericFormat === 'int64') {
1439
+ col.format = '1.0-0';
1440
+ }
1441
+ else {
1442
+ const explicitDecimals = this.firstFiniteNumber(field.decimalPlaces, field.extraProperties?.decimalPlaces);
1443
+ if (explicitDecimals !== undefined && explicitDecimals >= 0) {
1444
+ const decimals = Math.trunc(explicitDecimals);
1445
+ col.format = `1.${decimals}-${decimals}`;
1446
+ }
1447
+ }
1448
+ }
1449
+ const textMaskFormat = resolveTextMaskFormatFromFieldDefinition(field);
1450
+ const maskResolvedType = resolveColumnTypeFromFieldDefinition(field, col.type);
1451
+ if (textMaskFormat && maskResolvedType === 'string') {
1452
+ col.type = 'string';
1453
+ col.format = textMaskFormat;
1454
+ }
1455
+ }
1456
+ applyAutoRenderer(field, col) {
1457
+ if (!field || !col)
1458
+ return;
1459
+ // Do not override explicit renderer if any (defensive; initial columns have none)
1460
+ if (col.renderer && col.renderer.type)
1461
+ return;
1462
+ const ctl = (field.controlType || '').toString().trim().toLowerCase();
1463
+ const fname = (field.name || '').toString();
1464
+ const isAvatarByControl = ctl === 'avatar';
1465
+ if (isAvatarByControl) {
1466
+ const initialsField = this.firstNonEmptyString(field.initialsField, field.extraProperties?.initialsField) || 'nomeCompleto';
1467
+ col.renderer = {
1468
+ type: 'avatar',
1469
+ avatar: {
1470
+ srcField: fname,
1471
+ initialsField,
1472
+ shape: 'circle',
1473
+ size: 32,
1474
+ },
1475
+ };
1476
+ // Sensible defaults for avatar column (84px prevents header truncation when centered with sort/resize gutters; not sortable by default)
1477
+ col.align = col.align || 'center';
1478
+ if (col.sortable === undefined)
1479
+ col.sortable = false;
1480
+ if (!col.width)
1481
+ col.width = '84px';
1482
+ return;
1483
+ }
1484
+ }
1485
+ toPortableColumnDefinition(column) {
1486
+ const cloned = this.clonePlainObject(column);
1487
+ delete cloned['_isApiField'];
1488
+ delete cloned['_originalApiType'];
1489
+ return cloned;
1490
+ }
1491
+ normalizePresentationToken(value) {
1492
+ return String(value ?? '')
1493
+ .trim()
1494
+ .toLowerCase()
1495
+ .replace(/[_\-\s]/g, '');
1496
+ }
1497
+ firstNonEmptyString(...values) {
1498
+ for (const value of values) {
1499
+ if (typeof value !== 'string')
1500
+ continue;
1501
+ const trimmed = value.trim();
1502
+ if (trimmed)
1503
+ return trimmed;
1504
+ }
1505
+ return undefined;
1506
+ }
1507
+ resolveSchemaValueMapping(field) {
1508
+ const explicit = field.valueMapping;
1509
+ if (explicit && typeof explicit === 'object' && !Array.isArray(explicit)) {
1510
+ const normalized = Object.entries(explicit).reduce((acc, [key, value]) => {
1511
+ if (key && typeof value === 'string' && value.trim()) {
1512
+ acc[key] = value.trim();
1513
+ }
1514
+ return acc;
1515
+ }, {});
1516
+ if (Object.keys(normalized).length)
1517
+ return normalized;
1518
+ }
1519
+ const optionMapping = this.resolveSchemaOptionsValueMapping(field);
1520
+ if (optionMapping)
1521
+ return optionMapping;
1522
+ const enumValues = this.normalizeEnumValues(field.enumValues) || [];
1523
+ if (enumValues.length) {
1524
+ return enumValues.reduce((acc, value) => {
1525
+ acc[value] = this.humanizeSchemaEnumValue(value);
1526
+ return acc;
1527
+ }, {});
1528
+ }
1529
+ return undefined;
1530
+ }
1531
+ applySchemaSemanticCellPresentation(field, col, presentation) {
1532
+ if (!presentation || typeof presentation !== 'object')
1533
+ return;
1534
+ if (col.renderer?.type)
1535
+ return;
1536
+ const presenter = this.normalizePresentationToken(presentation.presenter ?? presentation.variant);
1537
+ const textField = field.name;
1538
+ const color = this.resolveSemanticPresentationColor(presentation.tone);
1539
+ const variant = this.resolveSemanticPresentationVariant(presentation.appearance);
1540
+ const icon = this.firstNonEmptyString(presentation.icon);
1541
+ const tooltipText = this.firstNonEmptyString(presentation.tooltip);
1542
+ const tooltip = tooltipText ? { text: tooltipText } : undefined;
1543
+ const fixedText = this.firstNonEmptyString(presentation.label);
1544
+ if (presenter === 'chip') {
1545
+ col.renderer = {
1546
+ type: 'chip',
1547
+ chip: {
1548
+ text: fixedText,
1549
+ textField: fixedText ? undefined : textField,
1550
+ color,
1551
+ icon,
1552
+ variant,
1553
+ tooltip,
1554
+ },
1555
+ };
1556
+ return;
1557
+ }
1558
+ if (presenter === 'badge' || presenter === 'status') {
1559
+ col.renderer = {
1560
+ type: 'badge',
1561
+ badge: {
1562
+ text: fixedText,
1563
+ textField: fixedText ? undefined : textField,
1564
+ color,
1565
+ icon,
1566
+ variant,
1567
+ tooltip,
1568
+ },
1569
+ };
1570
+ return;
1571
+ }
1572
+ if (presenter === 'iconvalue') {
1573
+ const rendererIcon = icon || this.firstNonEmptyString(field.icon);
1574
+ const affixes = this.resolveSemanticPresentationAffixes(presentation);
1575
+ const hasAffixes = !!(affixes.prefix || affixes.suffix);
1576
+ if (!rendererIcon && !hasAffixes)
1577
+ return;
1578
+ if (affixes.prefix || affixes.suffix || tooltipText) {
1579
+ col.renderer = {
1580
+ type: 'icon',
1581
+ icon: {
1582
+ ...(!hasAffixes && rendererIcon ? { name: rendererIcon } : {}),
1583
+ textField,
1584
+ ...(tooltipText ? { ariaLabel: tooltipText } : {}),
1585
+ ...affixes,
1586
+ },
1587
+ };
1588
+ return;
1589
+ }
1590
+ if (!rendererIcon)
1591
+ return;
1592
+ col.renderer = {
1593
+ type: 'compose',
1594
+ compose: {
1595
+ items: [
1596
+ {
1597
+ type: 'icon',
1598
+ icon: {
1599
+ name: rendererIcon,
1600
+ ...(color ? { color } : {}),
1601
+ ...(field.label ? { ariaLabel: field.label } : {}),
1602
+ },
1603
+ },
1604
+ {
1605
+ type: 'value',
1606
+ field: textField,
1607
+ },
1608
+ ],
1609
+ layout: {
1610
+ direction: 'row',
1611
+ gap: 6,
1612
+ align: 'center',
1613
+ },
1614
+ },
1615
+ };
1616
+ }
1617
+ }
1618
+ resolveSchemaCurrencyFormat(field) {
1619
+ const currency = this.firstNonEmptyString(field.currency, field.extraProperties?.currency) || 'BRL';
1620
+ const decimalPlaces = this.firstFiniteNumber(field.decimalPlaces, field.extraProperties?.decimalPlaces) ?? 2;
1621
+ return `${currency}|symbol|${decimalPlaces}`;
1622
+ }
1623
+ resolveSchemaBooleanFormat(field) {
1624
+ const explicit = this.firstNonEmptyString(field.booleanFormat, field.extraProperties?.booleanFormat);
1625
+ if (explicit)
1626
+ return explicit;
1627
+ const trueLabel = this.firstNonEmptyString(field.trueLabel, field.labelTrue, field.extraProperties?.trueLabel, field.extraProperties?.labelTrue);
1628
+ const falseLabel = this.firstNonEmptyString(field.falseLabel, field.labelFalse, field.extraProperties?.falseLabel, field.extraProperties?.labelFalse);
1629
+ if (trueLabel && falseLabel) {
1630
+ const tNorm = this.normalizePresentationToken(trueLabel);
1631
+ const fNorm = this.normalizePresentationToken(falseLabel);
1632
+ if (tNorm === 'ativo' && fNorm === 'inativo')
1633
+ return 'active-inactive';
1634
+ if (tNorm === 'sim' && (fNorm === 'nao' || fNorm === 'não'))
1635
+ return 'yes-no';
1636
+ return `custom|${trueLabel}|${falseLabel}`;
1637
+ }
1638
+ const name = this.normalizePresentationToken(field.name);
1639
+ if (name === 'ativo' || name === 'active' || name.endsWith('ativo')) {
1640
+ return 'active-inactive';
1641
+ }
1642
+ return 'true-false';
1643
+ }
1644
+ clonePlainObject(value) {
1645
+ try {
1646
+ return JSON.parse(JSON.stringify(value));
1647
+ }
1648
+ catch {
1649
+ return value;
1650
+ }
1651
+ }
1652
+ resolveSchemaOptionsValueMapping(field) {
1653
+ const options = this.normalizeSchemaOptions(field.options) || [];
1654
+ if (!options.length)
1655
+ return undefined;
1656
+ const valueKey = this.normalizeSchemaKey(field.optionValueKey) || 'key';
1657
+ const labelKey = this.normalizeSchemaKey(field.optionLabelKey) || 'value';
1658
+ const mapping = options.reduce((acc, option) => {
1659
+ if (!option || typeof option !== 'object')
1660
+ return acc;
1661
+ const value = option[valueKey]
1662
+ ?? option.key
1663
+ ?? option.id
1664
+ ?? option.value;
1665
+ const label = option[labelKey]
1666
+ ?? option.label
1667
+ ?? option.value
1668
+ ?? option.name;
1669
+ if (value !== undefined && value !== null && label !== undefined && label !== null) {
1670
+ acc[String(value)] = String(label);
1671
+ }
1672
+ return acc;
1673
+ }, {});
1674
+ return Object.keys(mapping).length ? mapping : undefined;
1675
+ }
1676
+ normalizeEnumValues(values) {
1677
+ if (!Array.isArray(values) || values.length === 0)
1678
+ return undefined;
1679
+ const normalized = values
1680
+ .map((value) => (value != null ? String(value).trim() : ''))
1681
+ .filter((value) => value.length > 0);
1682
+ return normalized.length ? Array.from(new Set(normalized)) : undefined;
1683
+ }
1684
+ humanizeSchemaEnumValue(value) {
1685
+ return value
1686
+ .toLowerCase()
1687
+ .split(/[_\-\s]+/)
1688
+ .filter(Boolean)
1689
+ .map((part, index) => index === 0 ? this.capitalizeSchemaEnumPart(part) : part)
1690
+ .join(' ');
1691
+ }
1692
+ resolveSemanticPresentationColor(tone) {
1693
+ const normalized = this.normalizePresentationToken(tone);
1694
+ if (normalized === 'neutral')
1695
+ return 'basic';
1696
+ if (!normalized)
1697
+ return undefined;
1698
+ if (normalized === 'danger')
1699
+ return 'warn';
1700
+ if (normalized === 'warning')
1701
+ return 'warn';
1702
+ return normalized;
1703
+ }
1704
+ resolveSemanticPresentationVariant(appearance) {
1705
+ const normalized = this.normalizePresentationToken(appearance);
1706
+ if (normalized === 'filled' || normalized === 'outlined' || normalized === 'soft' || normalized === 'plain') {
1707
+ return normalized;
1708
+ }
1709
+ return undefined;
1710
+ }
1711
+ resolveSemanticPresentationAffixes(presentation) {
1712
+ const prefix = this.firstNonEmptyString(presentation.prefix);
1713
+ const suffix = this.firstNonEmptyString(presentation.suffix);
1714
+ return {
1715
+ ...(prefix ? { prefix } : {}),
1716
+ ...(suffix ? { suffix } : {}),
1717
+ };
1718
+ }
1719
+ firstFiniteNumber(...values) {
1720
+ for (const value of values) {
1721
+ const numeric = typeof value === 'number' ? value : Number(value);
1722
+ if (Number.isFinite(numeric))
1723
+ return numeric;
1724
+ }
1725
+ return undefined;
1726
+ }
1727
+ normalizeSchemaOptions(options) {
1728
+ if (!Array.isArray(options) || options.length === 0)
1729
+ return undefined;
1730
+ const normalized = options
1731
+ .map((option) => {
1732
+ if (option == null)
1733
+ return null;
1734
+ if (typeof option === 'string') {
1735
+ const text = option.trim();
1736
+ if (!text)
1737
+ return null;
1738
+ return text;
1739
+ }
1740
+ if (typeof option === 'number') {
1741
+ return Number.isFinite(option) ? option : null;
1742
+ }
1743
+ if (typeof option === 'boolean') {
1744
+ return option;
1745
+ }
1746
+ if (typeof option === 'object') {
1747
+ const candidate = option;
1748
+ const hasKey = Object.prototype.hasOwnProperty.call(candidate, 'key');
1749
+ const hasValue = Object.prototype.hasOwnProperty.call(candidate, 'value');
1750
+ if (hasKey || hasValue) {
1751
+ const rawKey = candidate['key'] ?? candidate['value'];
1752
+ const rawValue = candidate['value'] ?? candidate['key'];
1753
+ const key = rawKey != null ? String(rawKey).trim() : '';
1754
+ const value = rawValue != null ? String(rawValue).trim() : '';
1755
+ if (!key && !value)
1756
+ return null;
1757
+ return { ...candidate, key: key || value, value: value || key };
1758
+ }
1759
+ return candidate;
1760
+ }
1761
+ return null;
1762
+ })
1763
+ .filter((option) => option !== null);
1764
+ return normalized.length ? normalized : undefined;
1765
+ }
1766
+ normalizeSchemaKey(value) {
1767
+ const text = value != null ? String(value).trim() : '';
1768
+ return text.length > 0 ? text : undefined;
1769
+ }
1770
+ capitalizeSchemaEnumPart(value) {
1771
+ return value ? `${value.charAt(0).toUpperCase()}${value.slice(1)}` : value;
1772
+ }
1773
+ }
1774
+ const tableSchemaColumnProjector = new TableSchemaColumnProjector();
1775
+
1776
+ /** Same JSON boundary and projection used when opening the native Table editor.
1777
+ * Empty fields preserve native columns, including the disconnected editor case.
1778
+ */
1779
+ function materializeTableEditorConfig(config, fields) {
1780
+ const materialized = { ...config };
1781
+ if (config.columnProjection?.source === 'schema' && fields.length > 0) {
1782
+ materialized.columns = tableSchemaColumnProjector.projectSchemaColumns(fields, config.columnProjection);
1783
+ }
1784
+ return JSON.parse(JSON.stringify(materialized));
1785
+ }
1786
+ /** Full native document: canonical defaults/action normalization are owned by
1787
+ * createTableAuthoringDocument. This is not a public host executor or attestation.
1788
+ */
1789
+ function deriveTableEditorDocument(input) {
1790
+ return createTableAuthoringDocument({
1791
+ config: materializeTableEditorConfig(input.config, input.fields),
1792
+ bindings: input.bindings,
1793
+ });
1794
+ }
1795
+
1796
+ /** Shared presentation ordering; hidden columns must not shift visible fallback positions. */
1797
+ function orderTableColumns(columns) {
1798
+ if (!columns.some(column => Number.isFinite(column.order)))
1799
+ return [...columns];
1800
+ let visibleIndex = 0;
1801
+ return columns.map((column, originalIndex) => {
1802
+ const fallback = column.visible === false ? originalIndex : visibleIndex++;
1803
+ return { column, originalIndex, explicit: Number.isFinite(column.order), order: Number.isFinite(column.order) ? Number(column.order) : fallback };
1804
+ }).sort((left, right) => left.order - right.order
1805
+ || Number(right.explicit) - Number(left.explicit)
1806
+ || left.originalIndex - right.originalIndex).map(entry => entry.column);
1807
+ }
1808
+
1809
+ /** Preserve authored values; runtime/schema defaults remain transient unless explicitly edited. */
1810
+ function applyTableAuthoringChanges(authored, baseline, edited) {
1811
+ return projectChanges(authored, baseline, edited, true);
1812
+ }
1813
+ function projectChanges(authored, baseline, edited, tableConfigRoot) {
1814
+ if (sameAuthoringValue(baseline, edited))
1815
+ return authored;
1816
+ if (!edited || typeof edited !== 'object' || Array.isArray(edited))
1817
+ return edited;
1818
+ const original = authored && typeof authored === 'object' ? authored : {};
1819
+ const before = baseline && typeof baseline === 'object' ? baseline : {};
1820
+ const after = edited;
1821
+ const result = { ...original };
1822
+ for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
1823
+ if (sameAuthoringValue(before[key], after[key]))
1824
+ continue;
1825
+ if (!Object.hasOwn(after, key))
1826
+ delete result[key];
1827
+ else
1828
+ Object.defineProperty(result, key, {
1829
+ value: projectChanges(Object.hasOwn(original, key) ? original[key] : undefined, Object.hasOwn(before, key) ? before[key] : undefined, after[key], false),
1830
+ enumerable: true, configurable: true, writable: true,
1831
+ });
1832
+ }
1833
+ if (tableConfigRoot && isRecord(original['columnProjection']) && original['columnProjection']['source'] === 'schema'
1834
+ && isRecord(result['columnProjection']) && result['columnProjection']['source'] === 'schema'
1835
+ && Array.isArray(after['columns']) && JSON.stringify(before['columns']) !== JSON.stringify(after['columns'])) {
1836
+ result['columns'] = [];
1837
+ result['columnProjection'] = projectSchemaColumnChanges(result['columnProjection'], before['columns'], after['columns'], isRecord(before['columnProjection']) ? before['columnProjection'] : original['columnProjection'], after['columnProjection']);
1838
+ }
1839
+ return result;
1840
+ }
1841
+ /** Write the existing compact schema projection, never a partial columns snapshot. */
1842
+ function projectSchemaColumnChanges(authored, baseline, edited, baselineProjection, editedProjection) {
1843
+ if (!isRecord(authored) || authored['source'] !== 'schema') {
1844
+ throw new Error('Table schema authorship requires its canonical columnProjection.');
1845
+ }
1846
+ const projection = structuredClone(authored);
1847
+ const overrides = Object.assign(Object.create(null), isRecord(projection['overrides']) ? projection['overrides'] : {});
1848
+ const additions = columnsByField(Array.isArray(projection['additions']) ? projection['additions'] : []);
1849
+ const before = columnsByField(Array.isArray(baseline) ? baseline : []);
1850
+ const after = columnsByField(edited);
1851
+ const nextAdditions = [...additions].filter(([field]) => !before.has(field) && !after.has(field))
1852
+ .map(([, column]) => structuredClone(column));
1853
+ for (const [field, column] of after) {
1854
+ if (additions.has(field) || !before.has(field)) {
1855
+ nextAdditions.push(structuredClone(column));
1856
+ continue;
1857
+ }
1858
+ const previousOverride = isRecord(overrides[field]) ? overrides[field] : {};
1859
+ if (Object.hasOwn(previousOverride, 'field')) {
1860
+ throw new Error('Table schema overrides cannot replace canonical field identity.');
1861
+ }
1862
+ const projected = projectChanges({ field, ...previousOverride }, before.get(field), column, false);
1863
+ const override = isRecord(projected) ? { ...projected } : {};
1864
+ // A direct edit of native intent is stronger evidence than a rematerialized snapshot.
1865
+ const baselineOverrides = isRecord(baselineProjection) && isRecord(baselineProjection['overrides'])
1866
+ ? baselineProjection['overrides'] : {};
1867
+ const editedOverrides = isRecord(editedProjection) && isRecord(editedProjection['overrides'])
1868
+ ? editedProjection['overrides'] : {};
1869
+ const oldIntent = isRecord(baselineOverrides[field]) ? baselineOverrides[field] : {};
1870
+ const newIntent = isRecord(editedOverrides[field]) ? editedOverrides[field] : {};
1871
+ if (isRecord(editedProjection)) {
1872
+ for (const key of new Set([...Object.keys(oldIntent), ...Object.keys(newIntent)])) {
1873
+ if (!changedNativeValue(oldIntent, newIntent, key))
1874
+ continue;
1875
+ if (Object.hasOwn(previousOverride, key))
1876
+ override[key] = structuredClone(previousOverride[key]);
1877
+ else
1878
+ delete override[key];
1879
+ }
1880
+ }
1881
+ delete override['field'];
1882
+ delete override['order']; // Ordering has a dedicated projection field.
1883
+ if (Object.keys(override).length)
1884
+ overrides[field] = override;
1885
+ else
1886
+ delete overrides[field];
1887
+ }
1888
+ // Missing fields do not erase their overrides; membership and intent are separate.
1889
+ const previousOrder = orderedFields(before, additions);
1890
+ const nextOrder = orderedFields(after, additions).filter(field => before.has(field));
1891
+ if (!changedNativeValue(baselineProjection, editedProjection, 'include')
1892
+ && previousOrder.some(field => !nextOrder.includes(field))) {
1893
+ const include = Array.isArray(projection['include']) ? projection['include'] : [];
1894
+ projection['include'] = [...nextOrder, ...include.filter(field => !before.has(field))];
1895
+ }
1896
+ if (!changedNativeValue(baselineProjection, editedProjection, 'order')
1897
+ && JSON.stringify(previousOrder) !== JSON.stringify(nextOrder)) {
1898
+ const order = Array.isArray(projection['order']) ? projection['order'] : [];
1899
+ projection['order'] = [...nextOrder, ...order.filter(field => !before.has(field) && !nextOrder.includes(field))];
1900
+ }
1901
+ if (Object.keys(overrides).length)
1902
+ projection['overrides'] = overrides;
1903
+ else
1904
+ delete projection['overrides'];
1905
+ if (nextAdditions.length)
1906
+ projection['additions'] = nextAdditions;
1907
+ else
1908
+ delete projection['additions'];
1909
+ return projection;
1910
+ }
1911
+ function changedNativeValue(before, after, key) {
1912
+ if (!isRecord(after))
1913
+ return false;
1914
+ const previous = isRecord(before) ? before : {};
1915
+ return Object.hasOwn(previous, key) !== Object.hasOwn(after, key)
1916
+ || !sameAuthoringValue(previous[key], after[key]);
1917
+ }
1918
+ function orderedFields(columns, additions) {
1919
+ return orderTableColumns([...columns].map(([field, column]) => ({
1920
+ field, order: typeof column['order'] === 'number' ? column['order'] : undefined,
1921
+ visible: typeof column['visible'] === 'boolean' ? column['visible'] : undefined,
1922
+ }))).map(column => column.field).filter(field => !additions.has(field));
1923
+ }
1924
+ function columnsByField(columns) {
1925
+ const result = new Map();
1926
+ for (const column of columns) {
1927
+ if (!isRecord(column) || typeof column['field'] !== 'string' || !column['field'].trim()) {
1928
+ throw new Error('Table authoring requires a non-empty column field identity.');
1929
+ }
1930
+ const identity = column['field'].trim();
1931
+ if (result.has(identity))
1932
+ throw new Error('Table authoring requires unique column field identities.');
1933
+ result.set(identity, column);
1934
+ }
1935
+ return result;
1936
+ }
1937
+ function isRecord(value) {
1938
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
1939
+ }
1940
+ /** Object insertion order is not authored intent; undefined is an internal absence sentinel. */
1941
+ function sameAuthoringValue(left, right) {
1942
+ if (left === undefined || right === undefined)
1943
+ return left === right;
1944
+ try {
1945
+ return uiLayoutJsonIdentity(left) === uiLayoutJsonIdentity(right);
1946
+ }
1947
+ catch {
1948
+ if (Array.isArray(left) || Array.isArray(right))
1949
+ return Array.isArray(left) && Array.isArray(right)
1950
+ && left.length === right.length && left.every((value, index) => sameAuthoringValue(value, right[index]));
1951
+ if (!isRecord(left) || !isRecord(right))
1952
+ return left === right;
1953
+ const keys = Object.keys(left).filter(key => left[key] !== undefined);
1954
+ const otherKeys = Object.keys(right).filter(key => right[key] !== undefined);
1955
+ return keys.length === otherKeys.length && keys.every(key => Object.prototype.hasOwnProperty.call(right, key)
1956
+ && sameAuthoringValue(left[key], right[key]));
1957
+ }
1958
+ }
1959
+
1960
+ const BINDINGS = ['resourcePath', 'horizontalScroll'];
1961
+ const INPUTS = ['config', ...BINDINGS];
1962
+ /** Raw compact schema authorship only; native normalization remains an editor/runtime projection. */
1963
+ function decodeTablePageInputs(value) {
1964
+ let inputs;
1965
+ try {
1966
+ inputs = cloneBoundedUiLayoutJsonDocument(value);
1967
+ }
1968
+ catch {
1969
+ return { document: null, diagnostics: [problem('table.page.json.invalid', '/definition/inputs')] };
1970
+ }
1971
+ const diagnostics = [];
1972
+ extras(inputs, INPUTS, '/definition/inputs', diagnostics);
1973
+ const config = inputs['config'];
1974
+ if (!record(config) || !Array.isArray(config['columns']) || config['columns'].length !== 0
1975
+ || !record(config['columnProjection']) || config['columnProjection']['source'] !== 'schema')
1976
+ diagnostics.push(problem('table.page.config.schema-required', '/definition/inputs/config'));
1977
+ if (record(config))
1978
+ nulls(config, '/definition/inputs/config', diagnostics);
1979
+ if (Object.prototype.hasOwnProperty.call(inputs, 'resourcePath') && inputs['resourcePath'] !== null && (typeof inputs['resourcePath'] !== 'string' || [...inputs['resourcePath']].length > 1024))
1980
+ diagnostics.push(problem('table.page.resourcePath.invalid', '/definition/inputs/resourcePath'));
1981
+ if (Object.prototype.hasOwnProperty.call(inputs, 'horizontalScroll') && !['auto', 'wrap', 'none'].includes(inputs['horizontalScroll']))
1982
+ diagnostics.push(problem('table.page.horizontalScroll.invalid', '/definition/inputs/horizontalScroll'));
1983
+ const bindings = Object.fromEntries(BINDINGS.filter(key => Object.prototype.hasOwnProperty.call(inputs, key)).map(key => [key, inputs[key]]));
1984
+ const document = { kind: 'praxis.table.editor', version: 1, config, ...(Object.keys(bindings).length ? { bindings } : {}) };
1985
+ if (!diagnostics.length) {
1986
+ try {
1987
+ diagnostics.push(...validateTableAuthoringDocument(document).filter(d => d.level === 'error').map(d => ({ ...d, path: nativePath(d.path) })));
1988
+ }
1989
+ catch {
1990
+ diagnostics.push(problem('table.page.native.invalid', '/definition/inputs/config'));
1991
+ }
1992
+ }
1993
+ if (!diagnostics.length) {
1994
+ try {
1995
+ cloneBoundedUiLayoutJsonDocument(document);
1996
+ }
1997
+ catch {
1998
+ diagnostics.push(problem('table.page.json.invalid', '/definition/inputs'));
1999
+ }
2000
+ }
2001
+ return { document: diagnostics.length ? null : document, diagnostics };
2002
+ }
2003
+ /** Core prefixes these widget-relative JSON Pointers; this callback never resolves a source/latest. */
2004
+ function validateTablePageWidget(widget) {
2005
+ if (widget.definition.id !== 'praxis-table')
2006
+ return [{ code: 'table.page.component.invalid', path: '/definition/id' }];
2007
+ return decodeTablePageInputs(widget.definition.inputs).diagnostics.map(d => ({ code: d.code, path: d.path ?? '/definition/inputs' }));
2008
+ }
2009
+ function encodeTablePageInputs(value) {
2010
+ let document;
2011
+ try {
2012
+ document = cloneBoundedUiLayoutJsonDocument(value);
2013
+ }
2014
+ catch {
2015
+ return { inputs: null, diagnostics: [problem('table.page.json.invalid', '/')] };
2016
+ }
2017
+ const diagnostics = [];
2018
+ extras(document, ['kind', 'version', 'config', 'bindings'], '', diagnostics);
2019
+ if (document['kind'] !== 'praxis.table.editor' || document['version'] !== 1)
2020
+ diagnostics.push(problem('table.page.native.invalid', '/'));
2021
+ const inputs = { config: document['config'] };
2022
+ if (Object.prototype.hasOwnProperty.call(document, 'bindings')) {
2023
+ const bindings = document['bindings'];
2024
+ if (!record(bindings) || !Object.keys(bindings).length)
2025
+ diagnostics.push(problem('table.page.envelope.unrepresentable', '/bindings'));
2026
+ else {
2027
+ extras(bindings, BINDINGS, '/bindings', diagnostics);
2028
+ Object.assign(inputs, bindings);
2029
+ }
2030
+ }
2031
+ const decoded = diagnostics.length ? null : decodeTablePageInputs(inputs);
2032
+ return { inputs: diagnostics.length || decoded?.diagnostics.length ? null : inputs, diagnostics: [...diagnostics, ...(decoded?.diagnostics ?? [])] };
2033
+ }
2034
+ /** No-change values in a materialized native editor cannot author defaults or erase absent keys. */
2035
+ function projectTablePageEditorChanges(authoredInputs, editorBaseline, editorValue) {
2036
+ const original = decodeTablePageInputs(authoredInputs);
2037
+ if (!original.document)
2038
+ return { inputs: null, diagnostics: original.diagnostics };
2039
+ let baseline;
2040
+ let edited;
2041
+ try {
2042
+ baseline = cloneBoundedUiLayoutJsonDocument(editorBaseline);
2043
+ edited = cloneBoundedUiLayoutJsonDocument(editorValue);
2044
+ }
2045
+ catch {
2046
+ return { inputs: null, diagnostics: [problem('table.page.json.invalid', '/')] };
2047
+ }
2048
+ const authored = original.document;
2049
+ // Native config delta has schema-column projection semantics; envelope delta is plain JSON.
2050
+ let candidate;
2051
+ try {
2052
+ candidate = applyTableAuthoringChanges(authored, baseline, edited);
2053
+ candidate['config'] = applyTableAuthoringChanges(authored['config'], baseline['config'], edited['config']);
2054
+ }
2055
+ catch {
2056
+ return { inputs: null, diagnostics: [problem('table.page.editor.invalid', '/definition/inputs/config')] };
2057
+ }
2058
+ const before = { ...authored };
2059
+ const after = { ...candidate };
2060
+ delete before['config'];
2061
+ delete after['config'];
2062
+ if (uiLayoutJsonIdentity(before) !== uiLayoutJsonIdentity(after))
2063
+ return { inputs: null, diagnostics: [problem('table.page.seed.replacement-required', '/definition/inputs')] };
2064
+ return encodeTablePageInputs(candidate);
2065
+ }
2066
+ function extras(source, allowed, path, diagnostics) {
2067
+ for (const key of Object.keys(source))
2068
+ if (!allowed.includes(key))
2069
+ diagnostics.push(problem('table.page.profile.unsupported', path + '/' + pointer(key)));
2070
+ }
2071
+ function nulls(value, path, diagnostics) {
2072
+ if (value === null)
2073
+ diagnostics.push(problem('table.page.config.null', path));
2074
+ else if (Array.isArray(value))
2075
+ value.forEach((item, index) => nulls(item, path + '/' + index, diagnostics));
2076
+ else if (record(value))
2077
+ Object.entries(value).forEach(([key, item]) => nulls(item, path + '/' + pointer(key), diagnostics));
2078
+ }
2079
+ function pointer(key) { return key.replace(/~/g, '~0').replace(/\//g, '~1'); }
2080
+ function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
2081
+ function problem(code, path) { return { level: 'error', code, path, message: code }; }
2082
+ /** Derive execution-only identity and disable independent preferences. Never persist this projection. */
2083
+ function materializeTablePageRuntimeInputs(value, pageId, widgetKey, options) {
2084
+ const decoded = decodeTablePageInputs(value);
2085
+ if (!decoded.document)
2086
+ return { inputs: null, diagnostics: decoded.diagnostics };
2087
+ let id;
2088
+ try {
2089
+ id = derivePageChildRuntimeId(pageId, widgetKey);
2090
+ }
2091
+ catch {
2092
+ return { inputs: null, diagnostics: [problem('table.page.identity.invalid', '/key')] };
2093
+ }
2094
+ const cloned = cloneBoundedUiLayoutJsonDocument(value);
2095
+ if (options?.preview && record(cloned['config'])) {
2096
+ const previewConfig = { ...cloned['config'] };
2097
+ if (record(previewConfig['actions'])) {
2098
+ const actions = { ...previewConfig['actions'] };
2099
+ if (record(actions['row'])) {
2100
+ actions['row'] = {
2101
+ ...actions['row'],
2102
+ edit: false,
2103
+ delete: false,
2104
+ duplicate: false,
2105
+ custom: [],
2106
+ };
2107
+ }
2108
+ if (record(actions['toolbar'])) {
2109
+ actions['toolbar'] = {
2110
+ ...actions['toolbar'],
2111
+ add: false,
2112
+ create: false,
2113
+ delete: false,
2114
+ import: false,
2115
+ custom: [],
2116
+ };
2117
+ }
2118
+ if (record(actions['bulk'])) {
2119
+ actions['bulk'] = {
2120
+ ...actions['bulk'],
2121
+ enabled: false,
2122
+ actions: [],
2123
+ };
2124
+ }
2125
+ previewConfig['actions'] = actions;
2126
+ }
2127
+ if (record(previewConfig['editing'])) {
2128
+ previewConfig['editing'] = {
2129
+ ...previewConfig['editing'],
2130
+ enabled: false,
2131
+ };
2132
+ }
2133
+ cloned['config'] = previewConfig;
2134
+ }
2135
+ return {
2136
+ inputs: {
2137
+ ...cloned,
2138
+ tableId: id,
2139
+ componentInstanceId: id,
2140
+ configPersistenceStrategy: 'volatile',
2141
+ enableCustomization: false,
2142
+ },
2143
+ diagnostics: [],
2144
+ };
2145
+ }
2146
+ const TABLE_GOVERNED_PAGE_PREVIEW_ADAPTER = {
2147
+ widgetDefinitionId: 'praxis-table',
2148
+ certifiedForInteractivePreview: true,
2149
+ validate: validateTablePageWidget,
2150
+ materializePreviewInputs: (widget, pageId) => {
2151
+ const materialized = materializeTablePageRuntimeInputs(widget.definition.inputs, pageId, widget.key, { preview: true });
2152
+ if (!materialized.inputs) {
2153
+ throw new Error(materialized.diagnostics[0]?.code ?? 'table.page.preview.invalid');
2154
+ }
2155
+ return materialized.inputs;
2156
+ },
2157
+ };
2158
+ function nativePath(value) {
2159
+ // The native dot-path protocol does not escape dynamic field identities. Its
2160
+ // overrides ancestor is precise; splitting that field would invent a pointer.
2161
+ if (value?.startsWith('config.columnProjection.overrides.'))
2162
+ return '/definition/inputs/config/columnProjection/overrides';
2163
+ const parts = (value ?? 'config').replace(/\[(\d+)\]/g, '.$1').split('.');
2164
+ if (parts[0] === 'bindings')
2165
+ parts.shift();
2166
+ return '/definition/inputs/' + parts.map(pointer).join('/');
2167
+ }
2168
+
2169
+ /**
2170
+ * Generated bundle index. Do not edit.
2171
+ */
2172
+
2173
+ export { TABLE_GOVERNED_PAGE_PREVIEW_ADAPTER, buildTableApplyPlan, createTableAuthoringDocument, decodeTablePageInputs, deriveTableEditorDocument, encodeTablePageInputs, materializeTablePageRuntimeInputs, normalizeTableAuthoringDocument, parseLegacyOrTableDocument, projectTablePageEditorChanges, serializeTableAuthoringDocument, toCanonicalTableConfig, validateTableAuthoringDocument, validateTablePageWidget };