@morit/cli 1.3.0 → 1.4.2

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.
Files changed (37) hide show
  1. package/README.md +3 -5
  2. package/assets/plugin_contract.json +33 -8
  3. package/bin/morit.js +0 -0
  4. package/package.json +1 -1
  5. package/src/cli.js +5 -0
  6. package/src/preview.js +254 -17
  7. package/src/workspace.js +218 -19
  8. package/assets/docs/README.md +0 -107
  9. package/assets/docs/ai-response-and-timeline.md +0 -203
  10. package/assets/docs/ai-skill-and-docx-workflow.md +0 -83
  11. package/assets/docs/app-builder.md +0 -56
  12. package/assets/docs/authentication.md +0 -140
  13. package/assets/docs/components.md +0 -216
  14. package/assets/docs/design-tokens-responsive.md +0 -171
  15. package/assets/docs/docs-index.json +0 -94
  16. package/assets/docs/examples-notion.md +0 -83
  17. package/assets/docs/examples-school-life.md +0 -79
  18. package/assets/docs/getting-started.md +0 -132
  19. package/assets/docs/information-hierarchy.md +0 -81
  20. package/assets/docs/instances-and-connectors.md +0 -93
  21. package/assets/docs/lifecycle-and-api.md +0 -169
  22. package/assets/docs/local-cli.md +0 -125
  23. package/assets/docs/manifest.md +0 -234
  24. package/assets/docs/packaging-and-testing.md +0 -121
  25. package/assets/docs/permissions-and-data.md +0 -149
  26. package/assets/docs/platform-compatibility.md +0 -62
  27. package/assets/docs/plugin-storage.md +0 -175
  28. package/assets/docs/project-structure.md +0 -102
  29. package/assets/docs/remote-mcp.md +0 -158
  30. package/assets/docs/school-life-privacy.md +0 -55
  31. package/assets/docs/screens-layout-navigation.md +0 -95
  32. package/assets/docs/sdk-and-mcp.md +0 -199
  33. package/assets/docs/tool-and-skill.md +0 -172
  34. package/assets/docs/troubleshooting.md +0 -121
  35. package/assets/docs/ui-extensions.md +0 -75
  36. package/assets/docs/ui-runtime-v2.md +0 -343
  37. package/assets/docs/verification.md +0 -133
package/README.md CHANGED
@@ -46,11 +46,9 @@ npx -y @morit/cli plugin sync . --pull # Cloud → local
46
46
  ## SDK와 문서
47
47
 
48
48
  Node SDK는 `@morit/cli/sdk` export에서 같은 contract, validator, preview, build 기능을 제공합니다.
49
- 배포 package `assets/plugin_contract.json`과 `assets/docs/`에는 빌드 시점의 공통 계약과 공식
50
- 개발 문서가 함께 들어갑니다. 문서는 `assets/docs/README.md`의 실제 개발 순서를 따라 읽습니다.
51
-
52
- Plugin MCP 설정과 Local/Remote Tool 흐름은 bundled `assets/docs/sdk-and-mcp.md`, manifest와 UI
53
- 계약은 `assets/docs/manifest.md`, `assets/docs/ui-runtime-v2.md`를 참고하세요.
49
+ 배포 package에는 `assets/plugin_contract.json` 계약이 들어갑니다. 개발 문서는
50
+ `https://developers.moring.co/docs/morit-plugin/start`가 canonical 원본이며 Local MCP의
51
+ `morit_docs_search`와 `morit_docs_get`도 같은 원본을 ETag 캐시로 읽습니다.
54
52
 
55
53
  ## 파일과 보안 경계
56
54
 
@@ -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,8 @@
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
+ "http_json_response": ["collection_path", "id_path", "url_path", "title_paths", "content_paths", "max_items", "summary_prefix", "followup", "followups", "summary_path", "data_path"],
49
+ "capability": ["id", "kind", "title", "description", "permissions", "timeout_seconds", "runtime", "input_schema", "output_schema"],
49
50
  "ui_extension": ["id", "point", "title", "order", "permissions", "config"],
50
51
  "credential": ["id", "label", "description", "kind", "required", "oauth_provider", "allow_multiple"],
51
52
  "slash_command": ["id", "command", "title", "description", "capability", "argument_template"],
@@ -116,10 +117,14 @@
116
117
  "catalog_id": "https://moring.co/a2ui/catalogs/morit-ai/v1",
117
118
  "selection": "ai_explicit_host_validated",
118
119
  "selection_limit": 4,
119
- "model_selection_fields": ["candidate_id", "component", "chart_type"],
120
- "host_injects_tool_data": true,
120
+ "skill": "morit_a2ui",
121
+ "tool": "render_a2ui",
122
+ "model_selection_fields": ["candidate_id", "component", "values", "chart_type"],
123
+ "host_injects_tool_data": false,
124
+ "tool_values_must_equal_candidate": false,
125
+ "tool_values_must_be_candidate_grounded": true,
121
126
  "fallback": "text_only",
122
- "built_in_components": ["weather", "chart", "image_gallery", "exchange_rate", "article_list", "article_card", "file_result", "image_preview", "video_preview"],
127
+ "built_in_components": ["weather", "world_clock", "chart", "image_gallery", "exchange_rate", "article_list", "article_card", "file_result", "image_preview", "video_preview"],
123
128
  "validation_gates": ["completed_or_partial", "non_empty", "component_data", "answer_relevance", "not_duplicate", "plugin_schema", "plugin_permission", "plugin_version"],
124
129
  "plugin_component_prefix": "plugin.",
125
130
  "plugin_metadata_fields": ["version", "description", "schema"],
@@ -278,6 +283,17 @@
278
283
  "top",
279
284
  "right",
280
285
  "bottom"
286
+ ,"required"
287
+ ,"min_length"
288
+ ,"max_length"
289
+ ,"minimum"
290
+ ,"maximum"
291
+ ,"error_text"
292
+ ,"suggestions"
293
+ ,"suggestions_source"
294
+ ,"searchable"
295
+ ,"options_source"
296
+ ,"submit_label"
281
297
  ],
282
298
  "app_bar_fields": ["title", "subtitle", "center_title", "pinned", "leading", "actions"],
283
299
  "navigation_fields": ["type", "items", "selected_state_key", "persist", "label_behavior", "rail_breakpoint"],
@@ -289,8 +305,11 @@
289
305
  "theme_border_fields": ["color", "width", "radius"],
290
306
  "theme_icon_fields": ["color", "size"],
291
307
  "theme_state_fields": ["disabled_opacity", "selected_color", "focus_color"],
292
- "action_fields": ["type", "capability", "query", "arguments", "store", "target", "source", "values", "persist", "transition", "replace"],
293
- "actions": ["invoke", "navigate", "set_state", "refresh", "back"],
308
+ "action_fields": ["type", "capability", "query", "arguments", "store", "target", "source", "values", "persist", "transition", "replace", "mode", "actions", "continue_on_error", "validate", "parameters", "result_state", "result"],
309
+ "actions": ["invoke", "navigate", "set_state", "refresh", "back", "flow"],
310
+ "condition_fields": ["path", "equals", "not_equals", "exists", "all", "any", "not"],
311
+ "condition_context_roots": ["platform", "screen", "permissions", "connections", "connected", "loading", "errors", "route"],
312
+ "action_flow_modes": ["sequential", "parallel"],
294
313
  "navigation_transitions": ["platform", "fade", "slide", "none"],
295
314
  "chart_types": ["bar", "line", "donut", "scatter"],
296
315
  "navigation_types": ["tabs", "bar", "rail", "drawer", "adaptive"],
@@ -408,7 +427,13 @@
408
427
  "data_sources": 8,
409
428
  "state_keys": 32,
410
429
  "navigation_items": 8,
411
- "app_bar_actions": 6
430
+ "app_bar_actions": 6,
431
+ "condition_depth": 4,
432
+ "condition_nodes": 32,
433
+ "action_flow_depth": 4,
434
+ "action_flow_actions": 16,
435
+ "actions_per_flow": 8,
436
+ "input_options": 32
412
437
  }
413
438
  },
414
439
  "signature": {
package/bin/morit.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morit/cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.2",
4
4
  "description": "Official Morit Developer CLI for Cloud Projects, validation, builds, and deployments",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -19,6 +19,7 @@ const HELP = `Official Morit Developer CLI
19
19
  Usage:
20
20
  morit login [--token <access-token>] [--api-url <origin>]
21
21
  morit logout
22
+ morit docs
22
23
  morit plugin setup [directory] --id <plugin.id> --name <name> --publisher <publisher>
23
24
  morit plugin add [directory]
24
25
  morit plugin validate [directory]
@@ -348,6 +349,10 @@ export async function runCli(argv, options = {}) {
348
349
  output(stdout, "Signed out of Morit CLI");
349
350
  return 0;
350
351
  }
352
+ if (area === "docs") {
353
+ output(stdout, { url: `${DEFAULT_API_URL}/docs/morit-plugin/start` }, argv.includes("--json"));
354
+ return 0;
355
+ }
351
356
  const parsed = parseArguments(rest);
352
357
  const json = Boolean(parsed.flags.json);
353
358
 
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
  }