@morit/cli 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  {
2
- "sdk_version": "1.7.13",
2
+ "sdk_version": "1.7.14",
3
3
  "manifest_schema_versions": [1, 2],
4
4
  "recommended_schema_version": 2,
5
5
  "package_content_type": "application/vnd.morit.plugin+zip",
@@ -198,6 +198,46 @@
198
198
  "empty"
199
199
  ],
200
200
  "ui_runtime": {
201
+ "common_props": ["alignment", "aspect_ratio", "background_color", "border_color", "border_radius", "border_width", "clip", "disabled_background_color", "elevation", "enabled", "exclude_semantics", "foreground_color", "gradient_colors", "height", "margin", "max_height", "max_width", "min_height", "min_width", "opacity", "padding", "selected", "selected_background_color", "selected_foreground_color", "semantic_label", "tooltip", "width"],
202
+ "common_props_excluded_components": ["positioned"],
203
+ "component_props": {
204
+ "column": ["cross_axis_alignment", "main_axis_alignment", "main_axis_size", "spacing"],
205
+ "row": ["cross_axis_alignment", "main_axis_alignment", "main_axis_size", "spacing", "stack_at"],
206
+ "wrap": ["spacing"],
207
+ "grid": ["columns", "empty_text", "item_height", "item_key", "limit", "min_item_width", "page_size", "page_state_key", "source", "spacing"],
208
+ "stack": [],
209
+ "positioned": ["bottom", "height", "left", "padding", "right", "tooltip", "top", "width"],
210
+ "scroll": ["scroll_direction"],
211
+ "padding": [],
212
+ "center": [],
213
+ "expanded": ["flex"],
214
+ "surface": ["color", "pressed_color", "spacing"],
215
+ "card": ["color", "icon", "pressed_color", "spacing", "subtitle", "title", "tone"],
216
+ "section": ["icon", "spacing", "subtitle", "title"],
217
+ "text": ["align", "color", "font_size", "font_weight", "line_height", "max_lines", "style", "text"],
218
+ "icon": ["color", "icon", "size", "tone"],
219
+ "image": ["asset", "fit", "url"],
220
+ "avatar": ["asset", "fit", "size", "url"],
221
+ "badge": ["label"],
222
+ "divider": [],
223
+ "spacer": ["size"],
224
+ "button": ["full_width", "icon", "label", "style"],
225
+ "chip": ["icon", "label"],
226
+ "metric": ["icon", "label", "supporting", "tone", "value"],
227
+ "progress": ["label", "value"],
228
+ "list": ["dense", "empty_text", "item_key", "limit", "min_item_width", "page_size", "page_state_key", "scroll_direction", "source", "spacing"],
229
+ "timeline": ["dense", "empty_text", "item_key", "limit", "source"],
230
+ "calendar": ["date_key", "persist", "source", "state_key", "title", "title_key"],
231
+ "chart": ["chart_type", "show_legend", "source", "title", "x_key", "y_key"],
232
+ "table": ["columns", "source", "title"],
233
+ "form": ["spacing", "submit_label"],
234
+ "dialog": ["icon", "label", "title"],
235
+ "sheet": ["icon", "label", "title"],
236
+ "field": ["error_text", "input_type", "label", "max_length", "max_lines", "maximum", "min_length", "minimum", "persist", "placeholder", "required", "state_key", "suggestions", "suggestions_source"],
237
+ "select": ["error_text", "label", "options", "options_source", "persist", "required", "searchable", "state_key"],
238
+ "switch": ["error_text", "label", "persist", "required", "state_key", "supporting"],
239
+ "empty": ["icon", "supporting", "text", "title"]
240
+ },
201
241
  "schema_version": 2,
202
242
  "config_fields": [
203
243
  "ui_schema",
@@ -210,7 +250,8 @@
210
250
  "initial_state",
211
251
  "data_sources",
212
252
  "a2ui",
213
- "view"
253
+ "view",
254
+ "bottom_bar"
214
255
  ],
215
256
  "node_fields": ["id", "type", "props", "children", "action", "visible_when"],
216
257
  "prop_fields": [
@@ -294,7 +335,10 @@
294
335
  ,"searchable"
295
336
  ,"options_source"
296
337
  ,"submit_label",
297
- "gradient_colors", "item_key", "font_size", "font_weight", "line_height", "selected_background_color", "selected_foreground_color", "disabled_background_color", "pressed_color"
338
+ "gradient_colors", "item_key", "font_size", "font_weight", "line_height", "selected_background_color", "selected_foreground_color", "disabled_background_color", "pressed_color",
339
+ "page_size",
340
+ "page_state_key",
341
+ "item_height"
298
342
  ],
299
343
  "app_bar_fields": ["title", "subtitle", "center_title", "pinned", "leading", "actions"],
300
344
  "navigation_fields": ["type", "items", "selected_state_key", "persist", "label_behavior", "rail_breakpoint"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morit/cli",
3
- "version": "1.5.0",
3
+ "version": "1.6.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
@@ -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><details class="runtime-inspector"><summary>Runtime Inspector</summary><pre></pre></details><script type="application/json" class="runtime-config">' + safeConfig + "</script></article>";
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.13",
155
+ min_morit_version: "1.7.14",
156
156
  max_morit_version: "1.999.999",
157
157
  permissions: [],
158
158
  capabilities: [],
@@ -738,6 +738,12 @@ function validateMetadata(pluginId, name, publisher, description) {
738
738
  if (typeof description !== "string" || description.length > 4000) throw new Error("description must not exceed 4000 characters");
739
739
  }
740
740
 
741
+ function usesUi1714(node) {
742
+ if (!node || typeof node !== "object") return false;
743
+ const props = node.props || {};
744
+ return ["page_size", "page_state_key", "item_height"].some(key => Object.hasOwn(props, key)) || (node.type === "grid" && props.source !== undefined) || (node.children || []).some(usesUi1714);
745
+ }
746
+
741
747
  function usesUi1713(node) {
742
748
  if (!node || typeof node !== "object") return false;
743
749
  const props = node.props || {};
@@ -791,6 +797,7 @@ function validateManifest(manifest, files) {
791
797
  const capabilities = objectCollection(manifest.capabilities, "manifest.capabilities", 64);
792
798
  const uiExtensions = objectCollection(manifest.ui_extensions, "manifest.ui_extensions", 64);
793
799
  const minimum = manifest.min_morit_version.split(".").map(Number);
800
+ if ((minimum[0] < 1 || (minimum[0] === 1 && (minimum[1] < 7 || (minimum[1] === 7 && minimum[2] < 14)))) && uiExtensions.some(extension => extension.config?.bottom_bar != null || usesUi1714(extension.config?.view))) throw new Error("pagination and bottom_bar require min_morit_version 1.7.14 or newer");
794
801
  if ((minimum[0] < 1 || (minimum[0] === 1 && (minimum[1] < 7 || (minimum[1] === 7 && minimum[2] < 13)))) && uiExtensions.some(extension => usesUi1713(extension.config?.view))) throw new Error("new UI properties require min_morit_version 1.7.13 or newer");
795
802
  const credentials = objectCollection(manifest.credentials === undefined ? [] : manifest.credentials, "manifest.credentials", 16);
796
803
  const slashCommands = objectCollection(manifest.slash_commands === undefined ? [] : manifest.slash_commands, "manifest.slash_commands", 32);
@@ -1320,7 +1327,7 @@ function validateUiConfig(extension, capabilities, routeIds, files, hasStorage =
1320
1327
  if (config.placement != null && !["card", "action"].includes(extension.point)) throw new Error("UI placement is only valid for home extensions");
1321
1328
  validateUiRuntimeV2(config, executable, routeIds, capabilitySchemas);
1322
1329
  validateResponseComponent(config, extension.point === "response");
1323
- for (const asset of uiRuntimeAssetPaths(config.view)) {
1330
+ for (const asset of [...uiRuntimeAssetPaths(config.view), ...uiRuntimeAssetPaths(config.bottom_bar)]) {
1324
1331
  if (!(asset in files)) throw new Error(`UI image asset is not packaged: ${asset}`);
1325
1332
  }
1326
1333
  return;
@@ -1500,7 +1507,9 @@ function validateUiRuntimeV2(config, executable, routes, capabilitySchemas = new
1500
1507
  validateUiAppBar(config.app_bar, executable, routes, sourceIds, stateKeys);
1501
1508
  validateUiNavigation(config.navigation, executable, routes, sourceIds, stateKeys);
1502
1509
  assertObject(config.view, "UI Runtime v2 view");
1503
- validateUiNode(config.view, 0, { value: 0 }, executable, routes, sourceIds, stateKeys, null);
1510
+ const nodeCounter = { value: 0 };
1511
+ validateUiNode(config.view, 0, nodeCounter, executable, routes, sourceIds, stateKeys, null);
1512
+ if (config.bottom_bar != null) validateUiNode(config.bottom_bar, 0, nodeCounter, executable, routes, sourceIds, stateKeys, null, "bottom_bar");
1504
1513
  validateUiCapabilityBindings(config, sources, capabilitySchemas);
1505
1514
  }
1506
1515
 
@@ -1579,6 +1588,13 @@ function validateThemeContrast(theme) {
1579
1588
  function collectThemeWarnings(manifest) {
1580
1589
  const warnings = [];
1581
1590
  for (const extension of manifest.ui_extensions || []) {
1591
+ function visit(node, path) {
1592
+ if (!node || typeof node !== "object") return;
1593
+ const allowed = contract.ui_runtime.component_props[node.type] || [];
1594
+ for (const key of Object.keys(node.props || {})) if (!( !contract.ui_runtime.common_props_excluded_components.includes(node.type) && contract.ui_runtime.common_props.includes(key)) && !allowed.includes(key)) warnings.push(`${path}.props.${key}: ignored by ${node.type}; remove or move to a supported component`);
1595
+ (node.children || []).forEach((child, index) => visit(child, `${path}.children[${index}]`));
1596
+ }
1597
+ for (const field of ["view", "bottom_bar"]) visit(extension.config?.[field], `${extension.id}.${field}`);
1582
1598
  const theme = extension?.config?.theme;
1583
1599
  if (!theme || typeof theme !== "object" || Array.isArray(theme)) continue;
1584
1600
  for (const [mode, variant] of [["base", theme], ["light", theme.light], ["dark", theme.dark]]) {
@@ -1663,7 +1679,16 @@ function uiRuntimeAssetPaths(view) {
1663
1679
  return result;
1664
1680
  }
1665
1681
 
1666
- function validateUiNode(node, depth, counter, executable, routes, sources, stateKeys, parentType) {
1682
+ function validateUiNode(node, depth, counter, executable, routes, sources, stateKeys, parentType, path = "view") {
1683
+ try { validateUiNodeAt(node, depth, counter, executable, routes, sources, stateKeys, parentType, path); }
1684
+ catch (error) {
1685
+ if (/^(view[.:]|bottom_bar)/.test(error.message)) throw error;
1686
+ const key = Object.keys(node?.props || {}).find(key => !contract.ui_runtime.prop_fields.includes(key) || error.message.split(" ").includes(key));
1687
+ throw new Error(`${key ? `${path}.props.${key}` : path}: ${error.message}`);
1688
+ }
1689
+ }
1690
+
1691
+ function validateUiNodeAt(node, depth, counter, executable, routes, sources, stateKeys, parentType, path) {
1667
1692
  const nodeKeys = new Set(contract.ui_runtime.node_fields);
1668
1693
  rejectUnknownFields(node, nodeKeys, "UI component");
1669
1694
  counter.value += 1;
@@ -1672,6 +1697,7 @@ function validateUiNode(node, depth, counter, executable, routes, sources, state
1672
1697
  if (node.id != null && (typeof node.id !== "string" || !UI_IDENTIFIER.test(node.id))) throw new Error("UI component id is invalid");
1673
1698
  const props = node.props === undefined ? {} : node.props;
1674
1699
  validateUiProps(node.type, props, sources, stateKeys);
1700
+ if (node.type === "grid" && props.source !== undefined && node.children?.length !== 1) throw new Error("data grid requires one item template");
1675
1701
  validateUiCondition(node.visible_when, stateKeys, sources);
1676
1702
  if (node.action != null && !["surface", "card", "button", "chip", "form", "field", "select", "switch"].includes(node.type)) {
1677
1703
  throw new Error("UI action requires an interactive component");
@@ -1691,13 +1717,20 @@ function validateUiNode(node, depth, counter, executable, routes, sources, state
1691
1717
  }
1692
1718
  if (node.type === "positioned" && parentType !== "stack") throw new Error("positioned UI component requires a stack parent");
1693
1719
  if (node.type === "expanded" && !["row", "column"].includes(parentType)) throw new Error("expanded UI component requires a row or column parent");
1694
- for (const child of children) {
1720
+ for (const [index, child] of children.entries()) {
1695
1721
  assertObject(child, "UI component child");
1696
- validateUiNode(child, depth + 1, counter, executable, routes, sources, stateKeys, node.type);
1722
+ validateUiNode(child, depth + 1, counter, executable, routes, sources, stateKeys, node.type, `${path}.children[${index}]`);
1697
1723
  }
1698
1724
  }
1699
1725
 
1700
1726
  function validateUiProps(nodeType, props, sources, stateKeys) {
1727
+ if (["page_size", "page_state_key", "item_height"].some(key => Object.hasOwn(props, key)) && !["list", "grid"].includes(nodeType)) throw new Error("pagination requires a list or grid");
1728
+ if (props.item_height !== undefined && nodeType !== "grid") throw new Error("item_height requires a data grid");
1729
+ if (nodeType === "grid" && props.source === undefined && ["page_size", "page_state_key", "item_height"].some(key => Object.hasOwn(props, key))) throw new Error("grid pagination requires source");
1730
+ if (Object.hasOwn(props, "page_size") !== Object.hasOwn(props, "page_state_key")) throw new Error("page_size and page_state_key must be supplied together");
1731
+ if (props.page_size !== undefined && (!Number.isInteger(props.page_size) || props.page_size < 1 || props.page_size > 100)) throw new Error("UI page_size is invalid");
1732
+ if (props.page_state_key !== undefined && !stateKeys.has(props.page_state_key)) throw new Error("UI page_state_key references unknown state");
1733
+ if (props.item_height !== undefined) validateUiNumber(props.item_height, "UI item_height", 48, 4096);
1701
1734
  const allowed = new Set(contract.ui_runtime.prop_fields);
1702
1735
  rejectUnknownFields(props, allowed, "UI component props");
1703
1736
  const surfaceProps = new Set([
@@ -1768,7 +1801,7 @@ function validateUiProps(nodeType, props, sources, stateKeys) {
1768
1801
  if (nodeType !== "positioned" && ["left", "top", "right", "bottom"].some((key) => props[key] !== undefined)) throw new Error("position offsets require a positioned UI component");
1769
1802
  if (nodeType !== "expanded" && props.flex !== undefined) throw new Error("flex requires an expanded UI component");
1770
1803
  if ((nodeType !== "scroll" && props.shrink_wrap !== undefined) || (!["scroll", "list"].includes(nodeType) && props.scroll_direction !== undefined)) throw new Error("scroll properties require a scroll UI component");
1771
- if (["list", "timeline", "calendar", "chart", "table"].includes(nodeType)) {
1804
+ if (["list", "timeline", "calendar", "chart", "table"].includes(nodeType) || (nodeType === "grid" && props.source !== undefined)) {
1772
1805
  if (typeof props.source !== "string" || !UI_BINDING.test(props.source) || !props.source.startsWith("data.") || !sources.has(props.source.split(".")[1])) throw new Error("UI list source is invalid");
1773
1806
  }
1774
1807
  const chartType = props.chart_type === undefined ? "bar" : props.chart_type;