@morit/cli 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/plugin_contract.json +47 -3
- package/package.json +1 -1
- package/src/preview.js +38 -1
- package/src/workspace.js +40 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sdk_version": "1.7.
|
|
2
|
+
"sdk_version": "1.7.14",
|
|
3
3
|
"manifest_schema_versions": [1, 2],
|
|
4
4
|
"recommended_schema_version": 2,
|
|
5
5
|
"package_content_type": "application/vnd.morit.plugin+zip",
|
|
@@ -198,6 +198,46 @@
|
|
|
198
198
|
"empty"
|
|
199
199
|
],
|
|
200
200
|
"ui_runtime": {
|
|
201
|
+
"common_props": ["alignment", "aspect_ratio", "background_color", "border_color", "border_radius", "border_width", "clip", "disabled_background_color", "elevation", "enabled", "exclude_semantics", "foreground_color", "gradient_colors", "height", "margin", "max_height", "max_width", "min_height", "min_width", "opacity", "padding", "selected", "selected_background_color", "selected_foreground_color", "semantic_label", "tooltip", "width"],
|
|
202
|
+
"common_props_excluded_components": ["positioned"],
|
|
203
|
+
"component_props": {
|
|
204
|
+
"column": ["cross_axis_alignment", "main_axis_alignment", "main_axis_size", "spacing"],
|
|
205
|
+
"row": ["cross_axis_alignment", "main_axis_alignment", "main_axis_size", "spacing", "stack_at"],
|
|
206
|
+
"wrap": ["spacing"],
|
|
207
|
+
"grid": ["columns", "empty_text", "item_height", "item_key", "limit", "min_item_width", "page_size", "page_state_key", "source", "spacing"],
|
|
208
|
+
"stack": [],
|
|
209
|
+
"positioned": ["bottom", "height", "left", "padding", "right", "tooltip", "top", "width"],
|
|
210
|
+
"scroll": ["scroll_direction"],
|
|
211
|
+
"padding": [],
|
|
212
|
+
"center": [],
|
|
213
|
+
"expanded": ["flex"],
|
|
214
|
+
"surface": ["color", "pressed_color", "spacing"],
|
|
215
|
+
"card": ["color", "icon", "pressed_color", "spacing", "subtitle", "title", "tone"],
|
|
216
|
+
"section": ["icon", "spacing", "subtitle", "title"],
|
|
217
|
+
"text": ["align", "color", "font_size", "font_weight", "line_height", "max_lines", "style", "text"],
|
|
218
|
+
"icon": ["color", "icon", "size", "tone"],
|
|
219
|
+
"image": ["asset", "fit", "url"],
|
|
220
|
+
"avatar": ["asset", "fit", "size", "url"],
|
|
221
|
+
"badge": ["label"],
|
|
222
|
+
"divider": [],
|
|
223
|
+
"spacer": ["size"],
|
|
224
|
+
"button": ["full_width", "icon", "label", "style"],
|
|
225
|
+
"chip": ["icon", "label"],
|
|
226
|
+
"metric": ["icon", "label", "supporting", "tone", "value"],
|
|
227
|
+
"progress": ["label", "value"],
|
|
228
|
+
"list": ["dense", "empty_text", "item_key", "limit", "min_item_width", "page_size", "page_state_key", "scroll_direction", "source", "spacing"],
|
|
229
|
+
"timeline": ["dense", "empty_text", "item_key", "limit", "source"],
|
|
230
|
+
"calendar": ["date_key", "persist", "source", "state_key", "title", "title_key"],
|
|
231
|
+
"chart": ["chart_type", "show_legend", "source", "title", "x_key", "y_key"],
|
|
232
|
+
"table": ["columns", "source", "title"],
|
|
233
|
+
"form": ["spacing", "submit_label"],
|
|
234
|
+
"dialog": ["icon", "label", "title"],
|
|
235
|
+
"sheet": ["icon", "label", "title"],
|
|
236
|
+
"field": ["error_text", "input_type", "label", "max_length", "max_lines", "maximum", "min_length", "minimum", "persist", "placeholder", "required", "state_key", "suggestions", "suggestions_source"],
|
|
237
|
+
"select": ["error_text", "label", "options", "options_source", "persist", "required", "searchable", "state_key"],
|
|
238
|
+
"switch": ["error_text", "label", "persist", "required", "state_key", "supporting"],
|
|
239
|
+
"empty": ["icon", "supporting", "text", "title"]
|
|
240
|
+
},
|
|
201
241
|
"schema_version": 2,
|
|
202
242
|
"config_fields": [
|
|
203
243
|
"ui_schema",
|
|
@@ -210,7 +250,8 @@
|
|
|
210
250
|
"initial_state",
|
|
211
251
|
"data_sources",
|
|
212
252
|
"a2ui",
|
|
213
|
-
"view"
|
|
253
|
+
"view",
|
|
254
|
+
"bottom_bar"
|
|
214
255
|
],
|
|
215
256
|
"node_fields": ["id", "type", "props", "children", "action", "visible_when"],
|
|
216
257
|
"prop_fields": [
|
|
@@ -294,7 +335,10 @@
|
|
|
294
335
|
,"searchable"
|
|
295
336
|
,"options_source"
|
|
296
337
|
,"submit_label",
|
|
297
|
-
"gradient_colors", "item_key", "font_size", "font_weight", "line_height", "selected_background_color", "selected_foreground_color", "disabled_background_color", "pressed_color"
|
|
338
|
+
"gradient_colors", "item_key", "font_size", "font_weight", "line_height", "selected_background_color", "selected_foreground_color", "disabled_background_color", "pressed_color",
|
|
339
|
+
"page_size",
|
|
340
|
+
"page_state_key",
|
|
341
|
+
"item_height"
|
|
298
342
|
],
|
|
299
343
|
"app_bar_fields": ["title", "subtitle", "center_title", "pinned", "leading", "actions"],
|
|
300
344
|
"navigation_fields": ["type", "items", "selected_state_key", "persist", "label_behavior", "rail_breakpoint"],
|
package/package.json
CHANGED
package/src/preview.js
CHANGED
|
@@ -99,6 +99,9 @@ function nodeHtml(node) {
|
|
|
99
99
|
}
|
|
100
100
|
if (type === "switch") return '<label class="ui-switch"' + attributes + '><input type="checkbox" data-state-key="' + escape(props.state_key) + '"' + (props.required ? " required" : "") + "> " + label + "</label>";
|
|
101
101
|
if (type === "empty") return '<div class="ui-empty"><strong>' + escape(props.title || "표시할 내용이 없어요.") + "</strong><p>" + escape(props.supporting) + "</p></div>";
|
|
102
|
+
if (["list", "grid"].includes(type) && props.source) {
|
|
103
|
+
return '<section class="ui-collection" data-collection="' + jsonAttribute(node) + '"><div class="collection-items"></div><nav class="collection-pages"></nav></section>';
|
|
104
|
+
}
|
|
102
105
|
if (["list", "timeline", "calendar", "chart", "table"].includes(type)) {
|
|
103
106
|
return '<section class="ui-collection"><small>' + escape(type + (props.source ? " · " + props.source : "")) + "</small>" + (children || '<p>' + escape(props.empty_text || "데이터가 연결되면 표시됩니다.") + "</p>") + "</section>";
|
|
104
107
|
}
|
|
@@ -198,6 +201,35 @@ function previewRuntime() {
|
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
function update() {
|
|
204
|
+
for (const collection of article.querySelectorAll('[data-collection]')) {
|
|
205
|
+
const node = JSON.parse(collection.dataset.collection);
|
|
206
|
+
const props = node.props || {};
|
|
207
|
+
const values = resolvePreviewPath(roots(), props.source);
|
|
208
|
+
const rows = Array.isArray(values) ? values.slice(0, props.page_size ? 10000 : (props.limit || 20)) : [];
|
|
209
|
+
const size = props.page_size || Math.max(rows.length, 1);
|
|
210
|
+
const pages = Math.max(1, Math.ceil(rows.length / size));
|
|
211
|
+
const page = Math.max(0, Math.min(pages - 1, Number(state[props.page_state_key]) || 0));
|
|
212
|
+
const content = collection.querySelector('.collection-items');
|
|
213
|
+
const pageRows = rows.slice(page * size, (page + 1) * size);
|
|
214
|
+
const signature = JSON.stringify([pageRows, state]);
|
|
215
|
+
if (collection._signature !== signature) {
|
|
216
|
+
collection._signature = signature;
|
|
217
|
+
content.innerHTML = pageRows.length ? pageRows.map(item => nodeHtml(resolvePreviewValue(node.children[0], { ...roots(), item }))).join('') : '<p>' + escape(props.empty_text || '표시할 내용이 없어요.') + '</p>';
|
|
218
|
+
}
|
|
219
|
+
content.style.display = 'grid'; content.style.gap = (props.spacing || 12) + 'px';
|
|
220
|
+
content.style.gridTemplateColumns = node.type === 'grid' ? 'repeat(auto-fit,minmax(min(100%,' + (props.min_item_width || 148) + 'px),1fr))' : '1fr';
|
|
221
|
+
if (props.height) { content.style.maxHeight = props.height + 'px'; content.style.overflow = 'auto'; }
|
|
222
|
+
const nav = collection.querySelector('.collection-pages');
|
|
223
|
+
nav.replaceChildren();
|
|
224
|
+
if (props.page_size && pages > 1) {
|
|
225
|
+
for (const [label, next, disabled] of [['이전 페이지', page - 1, page === 0], ['다음 페이지', page + 1, page + 1 >= pages]]) {
|
|
226
|
+
const button = document.createElement('button'); button.textContent = label; button.disabled = disabled;
|
|
227
|
+
button.onclick = () => { state[props.page_state_key] = next; update(); };
|
|
228
|
+
nav.append(button);
|
|
229
|
+
if (next < page) nav.append(Object.assign(document.createElement('span'), { textContent: (page + 1) + ' / ' + pages }));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
201
233
|
for (const element of article.querySelectorAll("[data-visible]")) {
|
|
202
234
|
element.hidden = !evaluatePreviewCondition(parse(element, "visible"), roots());
|
|
203
235
|
}
|
|
@@ -328,6 +360,10 @@ function previewRuntime() {
|
|
|
328
360
|
}
|
|
329
361
|
|
|
330
362
|
const RUNTIME_SCRIPT = [
|
|
363
|
+
escape.toString(),
|
|
364
|
+
jsonAttribute.toString(),
|
|
365
|
+
nodeAttributes.toString(),
|
|
366
|
+
nodeHtml.toString(),
|
|
331
367
|
resolvePreviewPath.toString(),
|
|
332
368
|
previewValuesEqual.toString(),
|
|
333
369
|
resolvePreviewValue.toString(),
|
|
@@ -345,6 +381,7 @@ const CSS = [
|
|
|
345
381
|
".screen-meta,.app-bar,.screen-body,.ui-nav{padding:var(--space)}.screen-meta{display:flex;gap:8px;background:var(--surface-variant);color:var(--on-surface-variant)}.screen-meta span{margin-right:auto}.app-bar{display:flex;align-items:center;border-bottom:1px solid var(--outline-variant)}.app-bar div:first-child{display:grid}.app-bar small{color:var(--on-surface-variant)}.app-actions{margin-left:auto;display:flex;gap:6px}",
|
|
346
382
|
".screen-body,.ui-layout,.ui-surface,.ui-collection{display:flex;flex-direction:column;gap:var(--space)}.ui-layout.row{flex-direction:row;align-items:center}.ui-layout.wrap,.ui-nav{display:flex;flex-flow:row wrap;gap:8px}.ui-layout.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}.ui-surface,.ui-collection,.ui-empty{padding:var(--space);border:1px solid var(--outline-variant);border-radius:var(--radius);background:color-mix(in srgb,var(--surface),var(--primary-container) 9%)}",
|
|
347
383
|
"h3,p{margin:0}.ui-text.heading{font-size:1.25em;font-weight:750}.ui-text.caption,small{color:var(--on-surface-variant)}button,.ui-chip{border:0;border-radius:999px;padding:8px 13px;background:var(--primary-container);color:var(--on-primary-container);font:inherit}.ui-icon{font:600 .75em monospace;color:var(--primary)}.ui-image{min-height:90px;display:grid;place-items:center;border-radius:var(--radius);background:var(--surface-variant);color:var(--on-surface-variant)}.ui-avatar{display:grid;place-items:center;width:42px;height:42px;border-radius:50%;background:var(--primary);color:var(--on-primary)}.ui-badge{padding:3px 7px;border-radius:99px;background:var(--error-container);color:var(--on-error-container)}.ui-metric{display:grid;padding:12px;border-radius:var(--radius);background:var(--secondary-container);color:var(--on-secondary-container)}.ui-metric strong{font-size:1.35em}.ui-field{display:grid;gap:5px}input,select{width:100%;padding:11px;border:1px solid var(--outline);border-radius:var(--radius);background:var(--surface);color:var(--on-surface)}progress{accent-color:var(--primary);width:100%}hr{width:100%;border:0;border-top:1px solid var(--outline-variant)}.ui-spacer{min-height:12px}.ui-nav{border-top:1px solid var(--outline-variant)}",
|
|
384
|
+
".bottom-bar{position:sticky;bottom:0;z-index:2;padding:12px var(--space);background:var(--surface);border-top:1px solid var(--outline-variant)}.collection-pages{display:flex;align-items:center;justify-content:center;gap:12px}.collection-pages button{min-height:48px}.collection-items>*{content-visibility:auto;contain-intrinsic-size:auto 80px}",
|
|
348
385
|
"[hidden]{display:none!important}[data-action]{cursor:pointer}.runtime-inspector{margin:0 var(--space) var(--space);border-top:1px solid var(--outline-variant);padding-top:8px}.runtime-inspector pre{max-height:260px;overflow:auto;white-space:pre-wrap;font:12px/1.4 ui-monospace,monospace;color:var(--on-surface-variant)}",
|
|
349
386
|
].join("");
|
|
350
387
|
|
|
@@ -356,7 +393,7 @@ export function renderPreviewHtml(manifest) {
|
|
|
356
393
|
const safeConfig = JSON.stringify(config).replaceAll("<", "\\u003c").replaceAll(">", "\\u003e").replaceAll("&", "\\u0026");
|
|
357
394
|
return '<article class="screen" data-extension="' + escape(extension.id) + '" style="' + themeVariables(config) + '">' +
|
|
358
395
|
'<div class="screen-meta"><span>' + escape(extension.point) + "</span><strong>" + escape(extension.title) + "</strong></div>" +
|
|
359
|
-
chromeHtml(config) + '<main class="screen-body">' + nodeHtml(config.view) + '</main
|
|
396
|
+
chromeHtml(config) + '<main class="screen-body">' + nodeHtml(config.view) + '</main>' + (config.bottom_bar ? '<footer class="bottom-bar">' + nodeHtml(config.bottom_bar) + '</footer>' : '') + '<details class="runtime-inspector"><summary>Runtime Inspector</summary><pre></pre></details><script type="application/json" class="runtime-config">' + safeConfig + "</script></article>";
|
|
360
397
|
}).join("")
|
|
361
398
|
: '<article class="screen"><div class="ui-empty"><strong>UI extension 없음</strong><p>UI Runtime v2 화면을 추가하면 여기에 표시됩니다.</p></div></article>';
|
|
362
399
|
const title = escape(manifest.name);
|
package/src/workspace.js
CHANGED
|
@@ -152,7 +152,7 @@ export class LocalWorkspace {
|
|
|
152
152
|
version: "1.0.0",
|
|
153
153
|
cloud_project_id: null,
|
|
154
154
|
required_secrets: [],
|
|
155
|
-
min_morit_version: "1.7.
|
|
155
|
+
min_morit_version: "1.7.14",
|
|
156
156
|
max_morit_version: "1.999.999",
|
|
157
157
|
permissions: [],
|
|
158
158
|
capabilities: [],
|
|
@@ -738,6 +738,12 @@ function validateMetadata(pluginId, name, publisher, description) {
|
|
|
738
738
|
if (typeof description !== "string" || description.length > 4000) throw new Error("description must not exceed 4000 characters");
|
|
739
739
|
}
|
|
740
740
|
|
|
741
|
+
function usesUi1714(node) {
|
|
742
|
+
if (!node || typeof node !== "object") return false;
|
|
743
|
+
const props = node.props || {};
|
|
744
|
+
return ["page_size", "page_state_key", "item_height"].some(key => Object.hasOwn(props, key)) || (node.type === "grid" && props.source !== undefined) || (node.children || []).some(usesUi1714);
|
|
745
|
+
}
|
|
746
|
+
|
|
741
747
|
function usesUi1713(node) {
|
|
742
748
|
if (!node || typeof node !== "object") return false;
|
|
743
749
|
const props = node.props || {};
|
|
@@ -791,6 +797,7 @@ function validateManifest(manifest, files) {
|
|
|
791
797
|
const capabilities = objectCollection(manifest.capabilities, "manifest.capabilities", 64);
|
|
792
798
|
const uiExtensions = objectCollection(manifest.ui_extensions, "manifest.ui_extensions", 64);
|
|
793
799
|
const minimum = manifest.min_morit_version.split(".").map(Number);
|
|
800
|
+
if ((minimum[0] < 1 || (minimum[0] === 1 && (minimum[1] < 7 || (minimum[1] === 7 && minimum[2] < 14)))) && uiExtensions.some(extension => extension.config?.bottom_bar != null || usesUi1714(extension.config?.view))) throw new Error("pagination and bottom_bar require min_morit_version 1.7.14 or newer");
|
|
794
801
|
if ((minimum[0] < 1 || (minimum[0] === 1 && (minimum[1] < 7 || (minimum[1] === 7 && minimum[2] < 13)))) && uiExtensions.some(extension => usesUi1713(extension.config?.view))) throw new Error("new UI properties require min_morit_version 1.7.13 or newer");
|
|
795
802
|
const credentials = objectCollection(manifest.credentials === undefined ? [] : manifest.credentials, "manifest.credentials", 16);
|
|
796
803
|
const slashCommands = objectCollection(manifest.slash_commands === undefined ? [] : manifest.slash_commands, "manifest.slash_commands", 32);
|
|
@@ -1320,7 +1327,7 @@ function validateUiConfig(extension, capabilities, routeIds, files, hasStorage =
|
|
|
1320
1327
|
if (config.placement != null && !["card", "action"].includes(extension.point)) throw new Error("UI placement is only valid for home extensions");
|
|
1321
1328
|
validateUiRuntimeV2(config, executable, routeIds, capabilitySchemas);
|
|
1322
1329
|
validateResponseComponent(config, extension.point === "response");
|
|
1323
|
-
for (const asset of uiRuntimeAssetPaths(config.view)) {
|
|
1330
|
+
for (const asset of [...uiRuntimeAssetPaths(config.view), ...uiRuntimeAssetPaths(config.bottom_bar)]) {
|
|
1324
1331
|
if (!(asset in files)) throw new Error(`UI image asset is not packaged: ${asset}`);
|
|
1325
1332
|
}
|
|
1326
1333
|
return;
|
|
@@ -1500,7 +1507,9 @@ function validateUiRuntimeV2(config, executable, routes, capabilitySchemas = new
|
|
|
1500
1507
|
validateUiAppBar(config.app_bar, executable, routes, sourceIds, stateKeys);
|
|
1501
1508
|
validateUiNavigation(config.navigation, executable, routes, sourceIds, stateKeys);
|
|
1502
1509
|
assertObject(config.view, "UI Runtime v2 view");
|
|
1503
|
-
|
|
1510
|
+
const nodeCounter = { value: 0 };
|
|
1511
|
+
validateUiNode(config.view, 0, nodeCounter, executable, routes, sourceIds, stateKeys, null);
|
|
1512
|
+
if (config.bottom_bar != null) validateUiNode(config.bottom_bar, 0, nodeCounter, executable, routes, sourceIds, stateKeys, null, "bottom_bar");
|
|
1504
1513
|
validateUiCapabilityBindings(config, sources, capabilitySchemas);
|
|
1505
1514
|
}
|
|
1506
1515
|
|
|
@@ -1579,6 +1588,13 @@ function validateThemeContrast(theme) {
|
|
|
1579
1588
|
function collectThemeWarnings(manifest) {
|
|
1580
1589
|
const warnings = [];
|
|
1581
1590
|
for (const extension of manifest.ui_extensions || []) {
|
|
1591
|
+
function visit(node, path) {
|
|
1592
|
+
if (!node || typeof node !== "object") return;
|
|
1593
|
+
const allowed = contract.ui_runtime.component_props[node.type] || [];
|
|
1594
|
+
for (const key of Object.keys(node.props || {})) if (!( !contract.ui_runtime.common_props_excluded_components.includes(node.type) && contract.ui_runtime.common_props.includes(key)) && !allowed.includes(key)) warnings.push(`${path}.props.${key}: ignored by ${node.type}; remove or move to a supported component`);
|
|
1595
|
+
(node.children || []).forEach((child, index) => visit(child, `${path}.children[${index}]`));
|
|
1596
|
+
}
|
|
1597
|
+
for (const field of ["view", "bottom_bar"]) visit(extension.config?.[field], `${extension.id}.${field}`);
|
|
1582
1598
|
const theme = extension?.config?.theme;
|
|
1583
1599
|
if (!theme || typeof theme !== "object" || Array.isArray(theme)) continue;
|
|
1584
1600
|
for (const [mode, variant] of [["base", theme], ["light", theme.light], ["dark", theme.dark]]) {
|
|
@@ -1663,7 +1679,16 @@ function uiRuntimeAssetPaths(view) {
|
|
|
1663
1679
|
return result;
|
|
1664
1680
|
}
|
|
1665
1681
|
|
|
1666
|
-
function validateUiNode(node, depth, counter, executable, routes, sources, stateKeys, parentType) {
|
|
1682
|
+
function validateUiNode(node, depth, counter, executable, routes, sources, stateKeys, parentType, path = "view") {
|
|
1683
|
+
try { validateUiNodeAt(node, depth, counter, executable, routes, sources, stateKeys, parentType, path); }
|
|
1684
|
+
catch (error) {
|
|
1685
|
+
if (/^(view[.:]|bottom_bar)/.test(error.message)) throw error;
|
|
1686
|
+
const key = Object.keys(node?.props || {}).find(key => !contract.ui_runtime.prop_fields.includes(key) || error.message.split(" ").includes(key));
|
|
1687
|
+
throw new Error(`${key ? `${path}.props.${key}` : path}: ${error.message}`);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function validateUiNodeAt(node, depth, counter, executable, routes, sources, stateKeys, parentType, path) {
|
|
1667
1692
|
const nodeKeys = new Set(contract.ui_runtime.node_fields);
|
|
1668
1693
|
rejectUnknownFields(node, nodeKeys, "UI component");
|
|
1669
1694
|
counter.value += 1;
|
|
@@ -1672,6 +1697,7 @@ function validateUiNode(node, depth, counter, executable, routes, sources, state
|
|
|
1672
1697
|
if (node.id != null && (typeof node.id !== "string" || !UI_IDENTIFIER.test(node.id))) throw new Error("UI component id is invalid");
|
|
1673
1698
|
const props = node.props === undefined ? {} : node.props;
|
|
1674
1699
|
validateUiProps(node.type, props, sources, stateKeys);
|
|
1700
|
+
if (node.type === "grid" && props.source !== undefined && node.children?.length !== 1) throw new Error("data grid requires one item template");
|
|
1675
1701
|
validateUiCondition(node.visible_when, stateKeys, sources);
|
|
1676
1702
|
if (node.action != null && !["surface", "card", "button", "chip", "form", "field", "select", "switch"].includes(node.type)) {
|
|
1677
1703
|
throw new Error("UI action requires an interactive component");
|
|
@@ -1691,13 +1717,20 @@ function validateUiNode(node, depth, counter, executable, routes, sources, state
|
|
|
1691
1717
|
}
|
|
1692
1718
|
if (node.type === "positioned" && parentType !== "stack") throw new Error("positioned UI component requires a stack parent");
|
|
1693
1719
|
if (node.type === "expanded" && !["row", "column"].includes(parentType)) throw new Error("expanded UI component requires a row or column parent");
|
|
1694
|
-
for (const child of children) {
|
|
1720
|
+
for (const [index, child] of children.entries()) {
|
|
1695
1721
|
assertObject(child, "UI component child");
|
|
1696
|
-
validateUiNode(child, depth + 1, counter, executable, routes, sources, stateKeys, node.type);
|
|
1722
|
+
validateUiNode(child, depth + 1, counter, executable, routes, sources, stateKeys, node.type, `${path}.children[${index}]`);
|
|
1697
1723
|
}
|
|
1698
1724
|
}
|
|
1699
1725
|
|
|
1700
1726
|
function validateUiProps(nodeType, props, sources, stateKeys) {
|
|
1727
|
+
if (["page_size", "page_state_key", "item_height"].some(key => Object.hasOwn(props, key)) && !["list", "grid"].includes(nodeType)) throw new Error("pagination requires a list or grid");
|
|
1728
|
+
if (props.item_height !== undefined && nodeType !== "grid") throw new Error("item_height requires a data grid");
|
|
1729
|
+
if (nodeType === "grid" && props.source === undefined && ["page_size", "page_state_key", "item_height"].some(key => Object.hasOwn(props, key))) throw new Error("grid pagination requires source");
|
|
1730
|
+
if (Object.hasOwn(props, "page_size") !== Object.hasOwn(props, "page_state_key")) throw new Error("page_size and page_state_key must be supplied together");
|
|
1731
|
+
if (props.page_size !== undefined && (!Number.isInteger(props.page_size) || props.page_size < 1 || props.page_size > 100)) throw new Error("UI page_size is invalid");
|
|
1732
|
+
if (props.page_state_key !== undefined && !stateKeys.has(props.page_state_key)) throw new Error("UI page_state_key references unknown state");
|
|
1733
|
+
if (props.item_height !== undefined) validateUiNumber(props.item_height, "UI item_height", 48, 4096);
|
|
1701
1734
|
const allowed = new Set(contract.ui_runtime.prop_fields);
|
|
1702
1735
|
rejectUnknownFields(props, allowed, "UI component props");
|
|
1703
1736
|
const surfaceProps = new Set([
|
|
@@ -1768,7 +1801,7 @@ function validateUiProps(nodeType, props, sources, stateKeys) {
|
|
|
1768
1801
|
if (nodeType !== "positioned" && ["left", "top", "right", "bottom"].some((key) => props[key] !== undefined)) throw new Error("position offsets require a positioned UI component");
|
|
1769
1802
|
if (nodeType !== "expanded" && props.flex !== undefined) throw new Error("flex requires an expanded UI component");
|
|
1770
1803
|
if ((nodeType !== "scroll" && props.shrink_wrap !== undefined) || (!["scroll", "list"].includes(nodeType) && props.scroll_direction !== undefined)) throw new Error("scroll properties require a scroll UI component");
|
|
1771
|
-
if (["list", "timeline", "calendar", "chart", "table"].includes(nodeType)) {
|
|
1804
|
+
if (["list", "timeline", "calendar", "chart", "table"].includes(nodeType) || (nodeType === "grid" && props.source !== undefined)) {
|
|
1772
1805
|
if (typeof props.source !== "string" || !UI_BINDING.test(props.source) || !props.source.startsWith("data.") || !sources.has(props.source.split(".")[1])) throw new Error("UI list source is invalid");
|
|
1773
1806
|
}
|
|
1774
1807
|
const chartType = props.chart_type === undefined ? "bar" : props.chart_type;
|