@morit/cli 1.4.2 → 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 +48 -3
- package/package.json +1 -1
- package/src/preview.js +38 -1
- package/src/workspace.js +62 -10
|
@@ -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": [
|
|
@@ -293,7 +334,11 @@
|
|
|
293
334
|
,"suggestions_source"
|
|
294
335
|
,"searchable"
|
|
295
336
|
,"options_source"
|
|
296
|
-
,"submit_label"
|
|
337
|
+
,"submit_label",
|
|
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"
|
|
297
342
|
],
|
|
298
343
|
"app_bar_fields": ["title", "subtitle", "center_title", "pinned", "leading", "actions"],
|
|
299
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,20 @@ 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
|
+
|
|
747
|
+
function usesUi1713(node) {
|
|
748
|
+
if (!node || typeof node !== "object") return false;
|
|
749
|
+
const props = node.props || {};
|
|
750
|
+
return ["gradient_colors", "item_key", "font_size", "font_weight", "line_height", "selected_background_color", "selected_foreground_color", "disabled_background_color", "pressed_color"].some(key => Object.hasOwn(props, key))
|
|
751
|
+
|| (node.type === "list" && props.scroll_direction === "horizontal")
|
|
752
|
+
|| (Array.isArray(node.children) && node.children.some(usesUi1713));
|
|
753
|
+
}
|
|
754
|
+
|
|
741
755
|
function validateManifest(manifest, files) {
|
|
742
756
|
assertObject(manifest, "manifest");
|
|
743
757
|
rejectUnknownFields(manifest, MANIFEST_FIELDS, "manifest");
|
|
@@ -782,6 +796,9 @@ function validateManifest(manifest, files) {
|
|
|
782
796
|
if (!["purge", "retain"].includes(manifest.data_policy)) throw new Error("manifest.data_policy must be purge or retain");
|
|
783
797
|
const capabilities = objectCollection(manifest.capabilities, "manifest.capabilities", 64);
|
|
784
798
|
const uiExtensions = objectCollection(manifest.ui_extensions, "manifest.ui_extensions", 64);
|
|
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");
|
|
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");
|
|
785
802
|
const credentials = objectCollection(manifest.credentials === undefined ? [] : manifest.credentials, "manifest.credentials", 16);
|
|
786
803
|
const slashCommands = objectCollection(manifest.slash_commands === undefined ? [] : manifest.slash_commands, "manifest.slash_commands", 32);
|
|
787
804
|
const connectors = objectCollection(manifest.connectors === undefined ? [] : manifest.connectors, "manifest.connectors", 32);
|
|
@@ -1310,7 +1327,7 @@ function validateUiConfig(extension, capabilities, routeIds, files, hasStorage =
|
|
|
1310
1327
|
if (config.placement != null && !["card", "action"].includes(extension.point)) throw new Error("UI placement is only valid for home extensions");
|
|
1311
1328
|
validateUiRuntimeV2(config, executable, routeIds, capabilitySchemas);
|
|
1312
1329
|
validateResponseComponent(config, extension.point === "response");
|
|
1313
|
-
for (const asset of uiRuntimeAssetPaths(config.view)) {
|
|
1330
|
+
for (const asset of [...uiRuntimeAssetPaths(config.view), ...uiRuntimeAssetPaths(config.bottom_bar)]) {
|
|
1314
1331
|
if (!(asset in files)) throw new Error(`UI image asset is not packaged: ${asset}`);
|
|
1315
1332
|
}
|
|
1316
1333
|
return;
|
|
@@ -1490,7 +1507,9 @@ function validateUiRuntimeV2(config, executable, routes, capabilitySchemas = new
|
|
|
1490
1507
|
validateUiAppBar(config.app_bar, executable, routes, sourceIds, stateKeys);
|
|
1491
1508
|
validateUiNavigation(config.navigation, executable, routes, sourceIds, stateKeys);
|
|
1492
1509
|
assertObject(config.view, "UI Runtime v2 view");
|
|
1493
|
-
|
|
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");
|
|
1494
1513
|
validateUiCapabilityBindings(config, sources, capabilitySchemas);
|
|
1495
1514
|
}
|
|
1496
1515
|
|
|
@@ -1569,6 +1588,13 @@ function validateThemeContrast(theme) {
|
|
|
1569
1588
|
function collectThemeWarnings(manifest) {
|
|
1570
1589
|
const warnings = [];
|
|
1571
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}`);
|
|
1572
1598
|
const theme = extension?.config?.theme;
|
|
1573
1599
|
if (!theme || typeof theme !== "object" || Array.isArray(theme)) continue;
|
|
1574
1600
|
for (const [mode, variant] of [["base", theme], ["light", theme.light], ["dark", theme.dark]]) {
|
|
@@ -1653,7 +1679,16 @@ function uiRuntimeAssetPaths(view) {
|
|
|
1653
1679
|
return result;
|
|
1654
1680
|
}
|
|
1655
1681
|
|
|
1656
|
-
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) {
|
|
1657
1692
|
const nodeKeys = new Set(contract.ui_runtime.node_fields);
|
|
1658
1693
|
rejectUnknownFields(node, nodeKeys, "UI component");
|
|
1659
1694
|
counter.value += 1;
|
|
@@ -1662,6 +1697,7 @@ function validateUiNode(node, depth, counter, executable, routes, sources, state
|
|
|
1662
1697
|
if (node.id != null && (typeof node.id !== "string" || !UI_IDENTIFIER.test(node.id))) throw new Error("UI component id is invalid");
|
|
1663
1698
|
const props = node.props === undefined ? {} : node.props;
|
|
1664
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");
|
|
1665
1701
|
validateUiCondition(node.visible_when, stateKeys, sources);
|
|
1666
1702
|
if (node.action != null && !["surface", "card", "button", "chip", "form", "field", "select", "switch"].includes(node.type)) {
|
|
1667
1703
|
throw new Error("UI action requires an interactive component");
|
|
@@ -1681,18 +1717,26 @@ function validateUiNode(node, depth, counter, executable, routes, sources, state
|
|
|
1681
1717
|
}
|
|
1682
1718
|
if (node.type === "positioned" && parentType !== "stack") throw new Error("positioned UI component requires a stack parent");
|
|
1683
1719
|
if (node.type === "expanded" && !["row", "column"].includes(parentType)) throw new Error("expanded UI component requires a row or column parent");
|
|
1684
|
-
for (const child of children) {
|
|
1720
|
+
for (const [index, child] of children.entries()) {
|
|
1685
1721
|
assertObject(child, "UI component child");
|
|
1686
|
-
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}]`);
|
|
1687
1723
|
}
|
|
1688
1724
|
}
|
|
1689
1725
|
|
|
1690
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);
|
|
1691
1734
|
const allowed = new Set(contract.ui_runtime.prop_fields);
|
|
1692
1735
|
rejectUnknownFields(props, allowed, "UI component props");
|
|
1693
1736
|
const surfaceProps = new Set([
|
|
1737
|
+
"selected",
|
|
1694
1738
|
"spacing", "padding", "margin", "width", "height", "min_width", "max_width", "min_height", "max_height",
|
|
1695
|
-
"alignment", "color", "background_color", "foreground_color", "border_color", "border_width",
|
|
1739
|
+
"alignment", "color", "gradient_colors", "selected_background_color", "selected_foreground_color", "disabled_background_color", "pressed_color", "background_color", "foreground_color", "border_color", "border_width",
|
|
1696
1740
|
"border_radius", "elevation", "opacity", "clip", "enabled", "tooltip", "semantic_label", "exclude_semantics",
|
|
1697
1741
|
]);
|
|
1698
1742
|
if (nodeType === "surface") rejectUnknownFields(props, surfaceProps, "surface UI component props");
|
|
@@ -1726,7 +1770,15 @@ function validateUiProps(nodeType, props, sources, stateKeys) {
|
|
|
1726
1770
|
if (props.main_axis_alignment !== undefined && !["start", "end", "center", "space_between", "space_around", "space_evenly"].includes(props.main_axis_alignment)) throw new Error("UI main-axis alignment is invalid");
|
|
1727
1771
|
if (props.cross_axis_alignment !== undefined && !["start", "end", "center", "stretch", "baseline"].includes(props.cross_axis_alignment)) throw new Error("UI cross-axis alignment is invalid");
|
|
1728
1772
|
if (props.main_axis_size !== undefined && !["min", "max"].includes(props.main_axis_size)) throw new Error("UI main-axis size is invalid");
|
|
1729
|
-
|
|
1773
|
+
if (props.font_size !== undefined) validateUiNumber(props.font_size, "UI font_size", 10, 64);
|
|
1774
|
+
if (props.line_height !== undefined) validateUiNumber(props.line_height, "UI line_height", 1, 2.5);
|
|
1775
|
+
if (props.font_weight !== undefined && (!Number.isInteger(props.font_weight) || props.font_weight < 100 || props.font_weight > 900 || props.font_weight % 100)) throw new Error("invalid UI font_weight");
|
|
1776
|
+
if (props.gradient_colors !== undefined) {
|
|
1777
|
+
if (!Array.isArray(props.gradient_colors) || props.gradient_colors.length < 2 || props.gradient_colors.length > 8) throw new Error("invalid UI gradient_colors: expected 2–8 colors");
|
|
1778
|
+
for (const color of props.gradient_colors) validateUiColor(color, "UI gradient_colors");
|
|
1779
|
+
}
|
|
1780
|
+
if (props.item_key !== undefined && (typeof props.item_key !== "string" || !/^[A-Za-z0-9_-]{1,80}$/.test(props.item_key))) throw new Error("invalid UI item_key");
|
|
1781
|
+
for (const key of ["color", "background_color", "foreground_color", "border_color", "selected_background_color", "selected_foreground_color", "disabled_background_color", "pressed_color"]) if (props[key] !== undefined) validateUiColor(props[key], `UI ${key}`);
|
|
1730
1782
|
if (props.border_width !== undefined) validateUiNumber(props.border_width, "UI border width", 0, 8);
|
|
1731
1783
|
if (props.border_radius !== undefined) validateUiNumber(props.border_radius, "UI border radius", 0, 64);
|
|
1732
1784
|
if (props.elevation !== undefined) validateUiNumber(props.elevation, "UI elevation", 0, 24);
|
|
@@ -1748,8 +1800,8 @@ function validateUiProps(nodeType, props, sources, stateKeys) {
|
|
|
1748
1800
|
)) throw new Error("positioned UI component over-constrains an axis");
|
|
1749
1801
|
if (nodeType !== "positioned" && ["left", "top", "right", "bottom"].some((key) => props[key] !== undefined)) throw new Error("position offsets require a positioned UI component");
|
|
1750
1802
|
if (nodeType !== "expanded" && props.flex !== undefined) throw new Error("flex requires an expanded UI component");
|
|
1751
|
-
if (nodeType !== "scroll" && ["
|
|
1752
|
-
if (["list", "timeline", "calendar", "chart", "table"].includes(nodeType)) {
|
|
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");
|
|
1804
|
+
if (["list", "timeline", "calendar", "chart", "table"].includes(nodeType) || (nodeType === "grid" && props.source !== undefined)) {
|
|
1753
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");
|
|
1754
1806
|
}
|
|
1755
1807
|
const chartType = props.chart_type === undefined ? "bar" : props.chart_type;
|