@morit/cli 1.2.0 → 1.4.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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "sdk_version": "1.7.8",
2
+ "sdk_version": "1.7.12",
3
3
  "manifest_schema_versions": [1, 2],
4
4
  "recommended_schema_version": 2,
5
5
  "package_content_type": "application/vnd.morit.plugin+zip",
@@ -45,7 +45,7 @@
45
45
  "slash_commands": {"target": "slash_commands", "kind": null}
46
46
  },
47
47
  "object_fields": {
48
- "capability": ["id", "kind", "title", "description", "permissions", "timeout_seconds", "runtime"],
48
+ "capability": ["id", "kind", "title", "description", "permissions", "timeout_seconds", "runtime", "input_schema", "output_schema"],
49
49
  "ui_extension": ["id", "point", "title", "order", "permissions", "config"],
50
50
  "credential": ["id", "label", "description", "kind", "required", "oauth_provider", "allow_multiple"],
51
51
  "slash_command": ["id", "command", "title", "description", "capability", "argument_template"],
@@ -112,10 +112,26 @@
112
112
  "permission_revalidation": true
113
113
  },
114
114
  "ai_response": {
115
+ "a2ui_version": "v0.9",
116
+ "catalog_id": "https://moring.co/a2ui/catalogs/morit-ai/v1",
117
+ "selection": "ai_explicit_host_validated",
118
+ "selection_limit": 4,
119
+ "skill": "morit_a2ui",
120
+ "tool": "render_a2ui",
121
+ "model_selection_fields": ["candidate_id", "component", "values", "chart_type"],
122
+ "host_injects_tool_data": false,
123
+ "tool_values_must_equal_candidate": false,
124
+ "tool_values_must_be_candidate_grounded": true,
125
+ "fallback": "text_only",
126
+ "built_in_components": ["weather", "world_clock", "chart", "image_gallery", "exchange_rate", "article_list", "article_card", "file_result", "image_preview", "video_preview"],
127
+ "validation_gates": ["completed_or_partial", "non_empty", "component_data", "answer_relevance", "not_duplicate", "plugin_schema", "plugin_permission", "plugin_version"],
128
+ "plugin_component_prefix": "plugin.",
129
+ "plugin_metadata_fields": ["version", "description", "schema"],
130
+ "plugin_schema_types": ["object", "array", "string", "number", "integer", "boolean", "null"],
131
+ "plugin_schema_max_bytes": 16384,
132
+ "plugin_schema_max_depth": 6,
115
133
  "state_field": "data.__morit_response.state",
116
- "states": ["loading", "partial", "completed", "failed"],
117
- "retryable_field": "data.__morit_response.retryable",
118
- "fallback_nodes": ["metric", "progress", "list", "timeline", "calendar", "chart", "table", "empty"],
134
+ "emitted_states": ["partial", "completed"],
119
135
  "image_artifact_mime_types": ["image/png", "image/jpeg", "image/gif", "image/webp"],
120
136
  "max_inline_image_bytes": 25165824,
121
137
  "max_inline_image_dimension": 4096,
@@ -192,6 +208,7 @@
192
208
  "theme",
193
209
  "initial_state",
194
210
  "data_sources",
211
+ "a2ui",
195
212
  "view"
196
213
  ],
197
214
  "node_fields": ["id", "type", "props", "children", "action", "visible_when"],
@@ -265,6 +282,17 @@
265
282
  "top",
266
283
  "right",
267
284
  "bottom"
285
+ ,"required"
286
+ ,"min_length"
287
+ ,"max_length"
288
+ ,"minimum"
289
+ ,"maximum"
290
+ ,"error_text"
291
+ ,"suggestions"
292
+ ,"suggestions_source"
293
+ ,"searchable"
294
+ ,"options_source"
295
+ ,"submit_label"
268
296
  ],
269
297
  "app_bar_fields": ["title", "subtitle", "center_title", "pinned", "leading", "actions"],
270
298
  "navigation_fields": ["type", "items", "selected_state_key", "persist", "label_behavior", "rail_breakpoint"],
@@ -276,8 +304,11 @@
276
304
  "theme_border_fields": ["color", "width", "radius"],
277
305
  "theme_icon_fields": ["color", "size"],
278
306
  "theme_state_fields": ["disabled_opacity", "selected_color", "focus_color"],
279
- "action_fields": ["type", "capability", "query", "arguments", "store", "target", "source", "values", "persist", "transition", "replace"],
280
- "actions": ["invoke", "navigate", "set_state", "refresh", "back"],
307
+ "action_fields": ["type", "capability", "query", "arguments", "store", "target", "source", "values", "persist", "transition", "replace", "mode", "actions", "continue_on_error", "validate", "parameters", "result_state", "result"],
308
+ "actions": ["invoke", "navigate", "set_state", "refresh", "back", "flow"],
309
+ "condition_fields": ["path", "equals", "not_equals", "exists", "all", "any", "not"],
310
+ "condition_context_roots": ["platform", "screen", "permissions", "connections", "connected", "loading", "errors", "route"],
311
+ "action_flow_modes": ["sequential", "parallel"],
281
312
  "navigation_transitions": ["platform", "fade", "slide", "none"],
282
313
  "chart_types": ["bar", "line", "donut", "scatter"],
283
314
  "navigation_types": ["tabs", "bar", "rail", "drawer", "adaptive"],
@@ -395,7 +426,13 @@
395
426
  "data_sources": 8,
396
427
  "state_keys": 32,
397
428
  "navigation_items": 8,
398
- "app_bar_actions": 6
429
+ "app_bar_actions": 6,
430
+ "condition_depth": 4,
431
+ "condition_nodes": 32,
432
+ "action_flow_depth": 4,
433
+ "action_flow_actions": 16,
434
+ "actions_per_flow": 8,
435
+ "input_options": 32
399
436
  }
400
437
  },
401
438
  "signature": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morit/cli",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Official Morit Developer CLI for Cloud Projects, validation, builds, and deployments",
5
5
  "type": "module",
6
6
  "bin": {
package/src/preview.js CHANGED
@@ -51,27 +51,53 @@ function themeVariables(config) {
51
51
  return values.join(";");
52
52
  }
53
53
 
54
+ function jsonAttribute(value) {
55
+ return escape(JSON.stringify(value));
56
+ }
57
+
58
+ function nodeAttributes(node) {
59
+ const attributes = [];
60
+ if (node.visible_when) attributes.push('data-visible="' + jsonAttribute(node.visible_when) + '"');
61
+ if (node.action) attributes.push('data-action="' + jsonAttribute(node.action) + '"');
62
+ return attributes.length ? " " + attributes.join(" ") : "";
63
+ }
64
+
54
65
  function nodeHtml(node) {
55
66
  if (!node || typeof node !== "object") return "";
56
67
  const type = String(node.type || "surface");
57
68
  const props = node.props && typeof node.props === "object" ? node.props : {};
58
69
  const children = Array.isArray(node.children) ? node.children.map(nodeHtml).join("") : "";
59
70
  const label = escape(props.label || props.title || type);
60
- const action = node.action ? ' data-action="' + escape(node.action.type) + '"' : "";
61
- if (type === "text") return '<p class="ui-text ' + escape(props.style) + '">' + escape(props.text) + "</p>";
62
- if (type === "icon") return '<span class="ui-icon" role="img" aria-label="' + escape(props.semantic_label || props.icon) + '">' + escape(props.icon) + "</span>";
63
- if (type === "image") return '<div class="ui-image" role="img" aria-label="' + escape(props.semantic_label || "image") + '">🖼 ' + escape(props.asset || props.url || "image") + "</div>";
64
- if (type === "avatar") return '<span class="ui-avatar">' + escape(props.label || "M").slice(0, 1) + "</span>";
65
- if (type === "badge") return '<span class="ui-badge">' + label + children + "</span>";
71
+ const attributes = nodeAttributes(node);
72
+ const template = (value) => ' data-template="' + escape(value ?? "") + '"';
73
+ if (type === "text") return '<p class="ui-text ' + escape(props.style) + '"' + attributes + template(props.text) + '>' + escape(props.text) + "</p>";
74
+ if (type === "icon") return '<span class="ui-icon" role="img" aria-label="' + escape(props.semantic_label || props.icon) + '"' + attributes + '>' + escape(props.icon) + "</span>";
75
+ if (type === "image") return '<div class="ui-image" role="img" aria-label="' + escape(props.semantic_label || "image") + '"' + attributes + '>🖼 ' + escape(props.asset || props.url || "image") + "</div>";
76
+ if (type === "avatar") return '<span class="ui-avatar"' + attributes + '>' + escape(props.label || "M").slice(0, 1) + "</span>";
77
+ if (type === "badge") return '<span class="ui-badge"' + attributes + '>' + label + children + "</span>";
66
78
  if (type === "divider") return "<hr>";
67
79
  if (type === "spacer") return '<span class="ui-spacer" aria-hidden="true"></span>';
68
- if (type === "button") return "<button" + action + ">" + (props.icon ? escape(props.icon) + " " : "") + label + "</button>";
69
- if (type === "chip") return '<span class="ui-chip"' + action + ">" + label + "</span>";
70
- if (type === "metric") return '<div class="ui-metric"><strong>' + escape(props.value) + "</strong><span>" + label + "</span></div>";
71
- if (type === "progress") return '<div class="ui-progress"><span>' + label + '</span><progress value="' + Number(props.value ?? .5) + '" max="1"></progress></div>';
72
- if (type === "field") return '<label class="ui-field">' + label + '<input disabled placeholder="' + escape(props.hint || props.placeholder) + '"></label>';
73
- if (type === "select") return '<label class="ui-field">' + label + '<select disabled><option>' + escape(props.placeholder || "선택") + "</option></select></label>";
74
- if (type === "switch") return '<label class="ui-switch"><input type="checkbox" disabled> ' + label + "</label>";
80
+ if (type === "button") return "<button type=\"button\"" + attributes + ">" + (props.icon ? escape(props.icon) + " " : "") + label + "</button>";
81
+ if (type === "chip") return '<button type="button" class="ui-chip"' + attributes + '>' + label + "</button>";
82
+ if (type === "metric") return '<div class="ui-metric"' + attributes + '><strong' + template(props.value) + '>' + escape(props.value) + "</strong><span>" + label + "</span></div>";
83
+ if (type === "progress") return '<div class="ui-progress"' + attributes + '><span>' + label + '</span><progress data-value-template="' + escape(props.value ?? .5) + '" value="' + Number(props.value ?? .5) + '" max="1"></progress></div>';
84
+ if (type === "field") {
85
+ const inputType = props.input_type === "number" ? "number" : props.input_type === "email" ? "email" : props.input_type === "url" ? "url" : "text";
86
+ const constraints = (props.required ? " required" : "") + (props.min_length != null ? ' minlength="' + Number(props.min_length) + '"' : "") + (props.max_length != null ? ' maxlength="' + Number(props.max_length) + '"' : "") + (props.minimum != null ? ' min="' + Number(props.minimum) + '"' : "") + (props.maximum != null ? ' max="' + Number(props.maximum) + '"' : "");
87
+ const suggestions = Array.isArray(props.suggestions) ? '<datalist id="list-' + escape(props.state_key) + '">' + props.suggestions.map((value) => '<option value="' + escape(value) + '"></option>').join("") + "</datalist>" : "";
88
+ const listId = props.suggestions || props.suggestions_source ? "list-" + props.state_key : "";
89
+ return '<label class="ui-field"' + attributes + '>' + label + '<input type="' + inputType + '" data-state-key="' + escape(props.state_key) + '" data-suggestions-source="' + escape(props.suggestions_source || "") + '" placeholder="' + escape(props.hint || props.placeholder) + '"' + constraints + (listId ? ' list="' + escape(listId) + '"' : "") + ">" + (suggestions || (listId ? '<datalist id="' + escape(listId) + '"></datalist>' : "")) + "</label>";
90
+ }
91
+ if (type === "select") {
92
+ const options = Array.isArray(props.options) ? props.options : [];
93
+ if (props.searchable) {
94
+ const listId = "select-" + props.state_key;
95
+ return '<label class="ui-field"' + attributes + '>' + label + '<input data-search-select="true" data-state-key="' + escape(props.state_key) + '" data-options-source="' + escape(props.options_source || "") + '" data-options="' + jsonAttribute(options) + '" list="' + escape(listId) + '" placeholder="' + escape(props.placeholder || "검색하여 선택") + '"' + (props.required ? " required" : "") + '><datalist id="' + escape(listId) + '">' + options.map((option) => '<option value="' + escape(option.label) + '"></option>').join("") + "</datalist></label>";
96
+ }
97
+ const optionHtml = options.map((option, index) => '<option value="' + index + '" data-value="' + jsonAttribute(option.value) + '">' + escape(option.label) + "</option>").join("");
98
+ return '<label class="ui-field"' + attributes + '>' + label + '<select data-state-key="' + escape(props.state_key) + '" data-options-source="' + escape(props.options_source || "") + '"' + (props.required ? " required" : "") + '><option value="">' + escape(props.placeholder || "선택") + "</option>" + optionHtml + "</select></label>";
99
+ }
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>";
75
101
  if (type === "empty") return '<div class="ui-empty"><strong>' + escape(props.title || "표시할 내용이 없어요.") + "</strong><p>" + escape(props.supporting) + "</p></div>";
76
102
  if (["list", "timeline", "calendar", "chart", "table"].includes(type)) {
77
103
  return '<section class="ui-collection"><small>' + escape(type + (props.source ? " · " + props.source : "")) + "</small>" + (children || '<p>' + escape(props.empty_text || "데이터가 연결되면 표시됩니다.") + "</p>") + "</section>";
@@ -83,7 +109,8 @@ function nodeHtml(node) {
83
109
  const heading = surface && (props.title || props.label)
84
110
  ? "<h3>" + label + "</h3>" + (props.subtitle ? "<p>" + escape(props.subtitle) + "</p>" : "")
85
111
  : "";
86
- return '<div class="' + className + '"' + action + ">" + heading + children + "</div>";
112
+ if (type === "form") return '<form class="' + className + '"' + attributes + ">" + heading + children + (props.submit_label ? '<button type="submit">' + escape(props.submit_label) + "</button>" : "") + "</form>";
113
+ return '<div class="' + className + '"' + attributes + ">" + heading + children + "</div>";
87
114
  }
88
115
 
89
116
  function chromeHtml(config) {
@@ -100,6 +127,214 @@ function chromeHtml(config) {
100
127
  return bar + nav;
101
128
  }
102
129
 
130
+ export function resolvePreviewPath(roots, path) {
131
+ const parts = String(path || "").split(".");
132
+ let value = roots[parts.shift()];
133
+ for (const part of parts) {
134
+ if (value == null) return null;
135
+ if (Array.isArray(value) && part === "length") value = value.length;
136
+ else if (Array.isArray(value) && /^\d+$/.test(part)) value = value[Number(part)];
137
+ else if (typeof value === "object") value = value[part];
138
+ else return null;
139
+ }
140
+ return value ?? null;
141
+ }
142
+
143
+ export function previewValuesEqual(left, right) {
144
+ if (Object.is(left, right)) return true;
145
+ if (Array.isArray(left) && Array.isArray(right)) {
146
+ return left.length === right.length && left.every((value, index) => previewValuesEqual(value, right[index]));
147
+ }
148
+ if (left && right && typeof left === "object" && typeof right === "object" && !Array.isArray(left) && !Array.isArray(right)) {
149
+ const keys = Object.keys(left);
150
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && previewValuesEqual(left[key], right[key]));
151
+ }
152
+ return typeof left === "number" && typeof right === "number" && left === right;
153
+ }
154
+
155
+ export function resolvePreviewValue(value, roots) {
156
+ if (Array.isArray(value)) return value.map((item) => resolvePreviewValue(item, roots));
157
+ if (value && typeof value === "object") {
158
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolvePreviewValue(item, roots)]));
159
+ }
160
+ if (typeof value !== "string") return value;
161
+ const exact = value.match(/^\{\{((?:state|data|context|item)\.[A-Za-z0-9_.-]+)\}\}$/);
162
+ if (exact) return resolvePreviewPath(roots, exact[1]);
163
+ return value.replace(/\{\{((?:state|data|context|item)\.[A-Za-z0-9_.-]+)\}\}/g, (_, path) => String(resolvePreviewPath(roots, path) ?? ""));
164
+ }
165
+
166
+ export function evaluatePreviewCondition(condition, roots) {
167
+ if (!condition) return true;
168
+ if (Array.isArray(condition.all)) return condition.all.every((item) => evaluatePreviewCondition(item, roots));
169
+ if (Array.isArray(condition.any)) return condition.any.some((item) => evaluatePreviewCondition(item, roots));
170
+ if (condition.not) return !evaluatePreviewCondition(condition.not, roots);
171
+ const value = resolvePreviewPath(roots, condition.path);
172
+ if (Object.hasOwn(condition, "equals")) return previewValuesEqual(value, resolvePreviewValue(condition.equals, roots));
173
+ if (Object.hasOwn(condition, "not_equals")) return !previewValuesEqual(value, resolvePreviewValue(condition.not_equals, roots));
174
+ if (Object.hasOwn(condition, "exists")) return condition.exists === (value !== null);
175
+ return true;
176
+ }
177
+
178
+ function previewRuntime() {
179
+ const parse = (element, key) => element.dataset[key] ? JSON.parse(element.dataset[key]) : null;
180
+ const same = previewValuesEqual;
181
+ for (const article of document.querySelectorAll(".screen[data-extension]")) {
182
+ const config = JSON.parse(article.querySelector(".runtime-config").textContent);
183
+ const state = structuredClone(config.initial_state || {});
184
+ const data = {};
185
+ const context = {
186
+ platform: "web",
187
+ permissions: {}, connections: {}, connected: false,
188
+ loading: {}, errors: {}, route: {},
189
+ screen: { width: article.clientWidth, height: innerHeight, size_class: article.clientWidth < 600 ? "compact" : article.clientWidth < 840 ? "medium" : "expanded" },
190
+ };
191
+ const roots = () => ({ state, data, context, item: {} });
192
+ const inspector = article.querySelector(".runtime-inspector pre");
193
+ const log = [];
194
+
195
+ function optionValue(option) {
196
+ if (!option?.dataset.value) return option?.value ?? null;
197
+ return JSON.parse(option.dataset.value);
198
+ }
199
+
200
+ function update() {
201
+ for (const element of article.querySelectorAll("[data-visible]")) {
202
+ element.hidden = !evaluatePreviewCondition(parse(element, "visible"), roots());
203
+ }
204
+ for (const element of article.querySelectorAll("[data-template]")) {
205
+ element.textContent = String(resolvePreviewValue(element.dataset.template, roots()) ?? "");
206
+ }
207
+ for (const element of article.querySelectorAll("progress[data-value-template]")) {
208
+ element.value = Number(resolvePreviewValue(element.dataset.valueTemplate, roots()) ?? 0);
209
+ }
210
+ for (const input of article.querySelectorAll("input[data-suggestions-source]")) {
211
+ if (!input.dataset.suggestionsSource) continue;
212
+ const values = resolvePreviewPath(roots(), input.dataset.suggestionsSource);
213
+ const list = document.getElementById(input.getAttribute("list"));
214
+ if (list && Array.isArray(values)) list.replaceChildren(...values.filter((value) => typeof value === "string").map((value) => Object.assign(document.createElement("option"), { value })));
215
+ }
216
+ for (const select of article.querySelectorAll("select[data-options-source]")) {
217
+ if (!select.dataset.optionsSource) continue;
218
+ const values = resolvePreviewPath(roots(), select.dataset.optionsSource);
219
+ const placeholder = select.options[0]?.cloneNode(true) || document.createElement("option");
220
+ const options = Array.isArray(values) ? values.filter((value) => value && typeof value === "object" && typeof value.label === "string") : [];
221
+ select.replaceChildren(placeholder, ...options.map((value, index) => {
222
+ const option = document.createElement("option");
223
+ option.value = String(index);
224
+ option.textContent = value.label;
225
+ option.dataset.value = JSON.stringify(value.value);
226
+ return option;
227
+ }));
228
+ }
229
+ for (const input of article.querySelectorAll("input[data-search-select]")) {
230
+ const values = input.dataset.optionsSource ? resolvePreviewPath(roots(), input.dataset.optionsSource) : JSON.parse(input.dataset.options || "[]");
231
+ input._previewOptions = Array.isArray(values) ? values.filter((value) => value && typeof value === "object" && typeof value.label === "string") : [];
232
+ const list = document.getElementById(input.getAttribute("list"));
233
+ if (list) list.replaceChildren(...input._previewOptions.map((value) => Object.assign(document.createElement("option"), { value: value.label })));
234
+ }
235
+ for (const input of article.querySelectorAll("[data-state-key]")) {
236
+ const value = state[input.dataset.stateKey];
237
+ if (input.type === "checkbox") input.checked = value === true;
238
+ else if (input.dataset.searchSelect) {
239
+ const match = (input._previewOptions || []).find((option) => same(option.value, value));
240
+ if (document.activeElement !== input && match && input.value !== match.label) input.value = match.label;
241
+ }
242
+ else if (input.tagName === "SELECT") {
243
+ const match = [...input.options].find((option) => same(optionValue(option), value));
244
+ if (match && input.value !== match.value) input.value = match.value;
245
+ } else if (document.activeElement !== input && input.value !== String(value ?? "")) input.value = String(value ?? "");
246
+ }
247
+ inspector.textContent = JSON.stringify({ state, data, context, log: log.slice(-8) }, null, 2);
248
+ }
249
+
250
+ async function perform(action) {
251
+ if (!action) return false;
252
+ if (action.validate && ![...article.querySelectorAll("form")].every((form) => form.reportValidity())) return false;
253
+ if (action.type === "flow") {
254
+ const tasks = action.actions || [];
255
+ if (action.mode === "parallel") {
256
+ const results = await Promise.all(tasks.map(perform));
257
+ return action.continue_on_error || results.every(Boolean);
258
+ }
259
+ for (const child of tasks) if (!await perform(child) && !action.continue_on_error) return false;
260
+ return true;
261
+ }
262
+ if (action.type === "set_state") Object.assign(state, resolvePreviewValue(action.values || {}, roots()));
263
+ else if (action.type === "invoke") {
264
+ const detail = { capability: action.capability, query: resolvePreviewValue(action.query, roots()), arguments: resolvePreviewValue(action.arguments || {}, roots()), result: null };
265
+ article.dispatchEvent(new CustomEvent("morit-preview-invoke", { detail, bubbles: true }));
266
+ if (action.store && detail.result != null) data[action.store] = detail.result;
267
+ log.push({ invoke: detail.capability, arguments: detail.arguments });
268
+ } else if (action.type === "refresh") {
269
+ const detail = { source: action.source, result: null };
270
+ article.dispatchEvent(new CustomEvent("morit-preview-refresh", { detail, bubbles: true }));
271
+ if (detail.result != null) data[action.source] = detail.result;
272
+ log.push({ refresh: action.source });
273
+ } else if (action.type === "navigate") {
274
+ const detail = { target: action.target, parameters: resolvePreviewValue(action.parameters || {}, roots()), result: null };
275
+ article.dispatchEvent(new CustomEvent("morit-preview-navigate", { detail, bubbles: true }));
276
+ context.route = detail.parameters;
277
+ document.querySelector('.screen[data-extension="' + CSS.escape(action.target) + '"]')?.scrollIntoView({ behavior: "smooth" });
278
+ if (action.result_state && detail.result != null) state[action.result_state] = detail.result;
279
+ log.push({ navigate: action.target, parameters: detail.parameters });
280
+ } else if (action.type === "back") {
281
+ article.dispatchEvent(new CustomEvent("morit-preview-back", { detail: { result: resolvePreviewValue(action.result || {}, roots()) }, bubbles: true }));
282
+ log.push({ back: true });
283
+ }
284
+ update();
285
+ return true;
286
+ }
287
+
288
+ article.addEventListener("input", (event) => {
289
+ const input = event.target.closest("[data-state-key]");
290
+ if (!input) return;
291
+ if (input.dataset.searchSelect) return;
292
+ state[input.dataset.stateKey] = input.type === "checkbox" ? input.checked : input.type === "number" && input.value !== "" ? Number(input.value) : input.value;
293
+ update();
294
+ });
295
+ article.addEventListener("change", (event) => {
296
+ const input = event.target.closest("[data-state-key]");
297
+ if (!input) return;
298
+ if (input.dataset.searchSelect) {
299
+ const match = (input._previewOptions || []).find((option) => option.label === input.value);
300
+ if (!match) { input.setCustomValidity("목록에서 선택해 주세요."); input.reportValidity(); return; }
301
+ input.setCustomValidity("");
302
+ state[input.dataset.stateKey] = match.value;
303
+ } else state[input.dataset.stateKey] = input.tagName === "SELECT" ? optionValue(input.selectedOptions[0]) : input.type === "checkbox" ? input.checked : input.type === "number" && input.value !== "" ? Number(input.value) : input.value;
304
+ const actionHost = input.closest("[data-action]");
305
+ if (actionHost) perform(parse(actionHost, "action"));
306
+ update();
307
+ });
308
+ article.addEventListener("submit", (event) => {
309
+ event.preventDefault();
310
+ perform(parse(event.target, "action"));
311
+ });
312
+ article.addEventListener("click", (event) => {
313
+ const host = event.target.closest("button[data-action], .ui-surface[data-action], .ui-layout[data-action]");
314
+ if (host) perform(parse(host, "action"));
315
+ });
316
+ article.addEventListener("keydown", (event) => {
317
+ if (event.key !== "Enter" || event.isComposing) return;
318
+ const field = event.target.closest(".ui-field[data-action]");
319
+ if (field) { event.preventDefault(); perform(parse(field, "action")); }
320
+ });
321
+ new ResizeObserver(() => {
322
+ context.screen.width = article.clientWidth;
323
+ context.screen.size_class = article.clientWidth < 600 ? "compact" : article.clientWidth < 840 ? "medium" : "expanded";
324
+ update();
325
+ }).observe(article);
326
+ update();
327
+ }
328
+ }
329
+
330
+ const RUNTIME_SCRIPT = [
331
+ resolvePreviewPath.toString(),
332
+ previewValuesEqual.toString(),
333
+ resolvePreviewValue.toString(),
334
+ evaluatePreviewCondition.toString(),
335
+ "(" + previewRuntime.toString() + ")();",
336
+ ].join("\n");
337
+
103
338
  const CSS = [
104
339
  "*{box-sizing:border-box}body{margin:0;font:15px/1.5 system-ui,sans-serif;background:#0d1211}",
105
340
  ".theme-toggle{position:fixed;right:18px;top:16px;z-index:3;padding:9px 13px;border-radius:999px;background:#fff;color:#17211f;box-shadow:0 4px 18px #0003;cursor:pointer}",
@@ -110,6 +345,7 @@ const CSS = [
110
345
  ".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}",
111
346
  ".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%)}",
112
347
  "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)}",
348
+ "[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)}",
113
349
  ].join("");
114
350
 
115
351
  export function renderPreviewHtml(manifest) {
@@ -117,9 +353,10 @@ export function renderPreviewHtml(manifest) {
117
353
  const screens = extensions.length
118
354
  ? extensions.map((extension) => {
119
355
  const config = extension.config;
120
- return '<article class="screen" style="' + themeVariables(config) + '">' +
356
+ const safeConfig = JSON.stringify(config).replaceAll("<", "\\u003c").replaceAll(">", "\\u003e").replaceAll("&", "\\u0026");
357
+ return '<article class="screen" data-extension="' + escape(extension.id) + '" style="' + themeVariables(config) + '">' +
121
358
  '<div class="screen-meta"><span>' + escape(extension.point) + "</span><strong>" + escape(extension.title) + "</strong></div>" +
122
- chromeHtml(config) + '<main class="screen-body">' + nodeHtml(config.view) + "</main></article>";
359
+ chromeHtml(config) + '<main class="screen-body">' + nodeHtml(config.view) + '</main><details class="runtime-inspector"><summary>Runtime Inspector</summary><pre></pre></details><script type="application/json" class="runtime-config">' + safeConfig + "</script></article>";
123
360
  }).join("")
124
361
  : '<article class="screen"><div class="ui-empty"><strong>UI extension 없음</strong><p>UI Runtime v2 화면을 추가하면 여기에 표시됩니다.</p></div></article>';
125
362
  const title = escape(manifest.name);
@@ -127,6 +364,6 @@ export function renderPreviewHtml(manifest) {
127
364
  title + " preview</title><style>" + CSS +
128
365
  '</style></head><body><input id="preview-dark" type="checkbox"><label class="theme-toggle" for="preview-dark">☀︎ / ☾</label><div class="preview"><header class="preview-head"><small>Morit UI Runtime v2 · 안전한 계약 Preview</small><h1>' +
129
366
  title + "</h1><p>" + escape(manifest.description || "") +
130
- '</p></header><section class="screens">' + screens + "</section></div></body></html>";
367
+ '</p></header><section class="screens">' + screens + "</section></div><script>" + RUNTIME_SCRIPT + "</script></body></html>";
131
368
  return Buffer.from(content, "utf8");
132
369
  }