@perspective-dev/viewer 5.3.1 → 5.5.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.
Files changed (291) hide show
  1. package/dist/cdn/perspective-viewer.js +6 -6
  2. package/dist/cdn/perspective-viewer.js.map +2 -2
  3. package/dist/css/blueprint.css +1 -1
  4. package/dist/css/botanical.css +1 -1
  5. package/dist/css/dracula.css +1 -1
  6. package/dist/css/eggplant.css +1 -1
  7. package/dist/css/gruvbox-dark.css +1 -1
  8. package/dist/css/gruvbox.css +1 -1
  9. package/dist/css/icons.css +1 -1
  10. package/dist/css/intl/ar.css +1 -0
  11. package/dist/css/intl/de.css +1 -1
  12. package/dist/css/intl/es.css +1 -1
  13. package/dist/css/intl/fr.css +1 -1
  14. package/dist/css/intl/ja.css +1 -1
  15. package/dist/css/intl/pt.css +1 -1
  16. package/dist/css/intl/zh.css +1 -1
  17. package/dist/css/intl.css +1 -1
  18. package/dist/css/ledger.css +1 -1
  19. package/dist/css/monokai.css +1 -1
  20. package/dist/css/nord.css +1 -1
  21. package/dist/css/phosphor.css +1 -1
  22. package/dist/css/pro-dark.css +1 -1
  23. package/dist/css/pro.css +1 -1
  24. package/dist/css/solarized-dark.css +1 -1
  25. package/dist/css/solarized.css +1 -1
  26. package/dist/css/themes.css +1 -1
  27. package/dist/css/vaporwave.css +1 -1
  28. package/dist/css/velvet.css +1 -1
  29. package/dist/docs/perspective-docs.json +1 -1
  30. package/dist/esm/extensions.d.ts +0 -5
  31. package/dist/esm/perspective-viewer.d.ts +2 -0
  32. package/dist/esm/perspective-viewer.inline.js +8 -8
  33. package/dist/esm/perspective-viewer.inline.js.map +2 -2
  34. package/dist/esm/perspective-viewer.js +5 -5
  35. package/dist/esm/perspective-viewer.js.map +2 -2
  36. package/dist/esm/plugin.d.ts +9 -0
  37. package/dist/esm/ts-rs/AddPanelOptions.d.ts +9 -0
  38. package/dist/esm/ts-rs/GetViewMode.d.ts +4 -0
  39. package/dist/esm/ts-rs/GetViewOptions.d.ts +8 -0
  40. package/dist/esm/ts-rs/RestoreOptions.d.ts +6 -0
  41. package/dist/esm/ts-rs/RestoreWorkspaceOptions.d.ts +9 -0
  42. package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +1 -1
  43. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +30 -22
  44. package/dist/wasm/perspective-viewer.d.ts +137 -104
  45. package/dist/wasm/perspective-viewer.js +337 -269
  46. package/dist/wasm/perspective-viewer.wasm +0 -0
  47. package/dist/wasm/perspective-viewer.wasm.d.ts +20 -30
  48. package/package.json +7 -7
  49. package/src/css/_context_menu.css +1 -1
  50. package/src/css/_dropdown_menu.css +1 -1
  51. package/src/css/_viewer_bundle.css +7 -8
  52. package/src/css/column-dropdown.css +0 -3
  53. package/src/css/column-selector.css +18 -7
  54. package/src/css/column-settings-panel.css +0 -225
  55. package/src/css/column-style.css +175 -0
  56. package/src/css/config-selector.css +19 -11
  57. package/src/css/filter-dropdown.css +5 -5
  58. package/src/css/form/debug.css +37 -4
  59. package/src/css/function-dropdown.css +5 -5
  60. package/src/css/panel-tab.css +23 -0
  61. package/src/css/plugin-selector.css +5 -5
  62. package/src/css/render-warning.css +1 -1
  63. package/src/css/status-bar.css +63 -21
  64. package/src/css/type-icon.css +1 -3
  65. package/src/css/{containers → ui/containers}/context-menu.css +22 -16
  66. package/src/css/ui/containers/dropdown-menu-items.css +66 -0
  67. package/src/css/{containers → ui/containers}/dropdown-menu.css +8 -53
  68. package/src/css/{containers → ui/containers}/tabs.css +2 -1
  69. package/src/css/viewer.css +32 -26
  70. package/src/rust/agent/tools.rs +80 -7
  71. package/src/rust/components/chat_panel.rs +1 -1
  72. package/src/rust/components/{form/code_editor.rs → code_editor.rs} +4 -2
  73. package/src/rust/components/column_dropdown.rs +2 -2
  74. package/src/rust/components/column_selector/active_column.rs +24 -9
  75. package/src/rust/components/column_selector/aggregate_selector.rs +1 -1
  76. package/src/rust/components/column_selector/config_selector.rs +2 -2
  77. package/src/rust/components/column_selector/filter_column.rs +8 -5
  78. package/src/rust/components/column_selector/inactive_column.rs +4 -2
  79. package/src/rust/components/column_selector/pivot_column.rs +4 -3
  80. package/src/rust/components/column_selector/sort_column.rs +7 -4
  81. package/src/rust/components/column_selector.rs +14 -3
  82. package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +1 -1
  83. package/src/rust/components/column_settings_sidebar/style_tab/alignment_field.rs +90 -0
  84. package/src/rust/components/column_settings_sidebar/style_tab/font_field.rs +267 -0
  85. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +16 -18
  86. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +1 -1
  87. package/src/rust/components/column_settings_sidebar/style_tab.rs +70 -12
  88. package/src/rust/components/column_settings_sidebar.rs +3 -4
  89. package/src/rust/components/copy_dropdown.rs +1 -1
  90. package/src/rust/components/datetime_column_style/custom.rs +1 -3
  91. package/src/rust/components/datetime_column_style/simple.rs +1 -2
  92. package/src/rust/components/datetime_column_style.rs +1 -2
  93. package/src/rust/components/{form/debug.rs → debug_panel.rs} +112 -55
  94. package/src/rust/components/{containers/dragdrop_list.rs → dragdrop_list.rs} +2 -10
  95. package/src/rust/components/export_dropdown.rs +1 -1
  96. package/src/rust/components/expression_editor.rs +1 -1
  97. package/src/rust/components/filter_dropdown.rs +2 -2
  98. package/src/rust/components/function_dropdown.rs +2 -2
  99. package/src/rust/components/main_panel/frame_theme.rs +3 -59
  100. package/src/rust/components/main_panel/msg.rs +4 -0
  101. package/src/rust/components/main_panel/reconcile.rs +9 -4
  102. package/src/rust/components/main_panel/render.rs +62 -11
  103. package/src/rust/components/main_panel/update.rs +39 -10
  104. package/src/rust/components/main_panel.rs +40 -2
  105. package/src/rust/components/mod.rs +5 -8
  106. package/src/rust/components/new_panel_menu.rs +97 -0
  107. package/src/rust/components/number_series_style.rs +2 -3
  108. package/src/rust/components/panel_menu.rs +61 -87
  109. package/src/rust/components/panel_tab.rs +100 -43
  110. package/src/rust/components/plugin_tab.rs +58 -4
  111. package/src/rust/components/render_warning.rs +2 -2
  112. package/src/rust/components/{status_bar_counter.rs → rows_counter.rs} +10 -10
  113. package/src/rust/components/settings_panel.rs +8 -2
  114. package/src/rust/components/status_bar.rs +113 -19
  115. package/src/rust/components/status_indicator.rs +9 -14
  116. package/src/rust/components/{style/mod.rs → style.rs} +32 -15
  117. package/src/rust/components/style_controls/number_string_format/digits_section.rs +3 -5
  118. package/src/rust/components/style_controls/number_string_format/misc_section.rs +1 -1
  119. package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -1
  120. package/src/rust/components/viewer/msg.rs +43 -30
  121. package/src/rust/components/viewer/panels.rs +53 -19
  122. package/src/rust/components/viewer/render.rs +16 -13
  123. package/src/rust/components/viewer/settings.rs +137 -68
  124. package/src/rust/components/viewer/snapshots.rs +5 -10
  125. package/src/rust/components/viewer/wiring.rs +11 -22
  126. package/src/rust/components/viewer.rs +15 -10
  127. package/src/rust/components/window_editor.rs +8 -9
  128. package/src/rust/config/column_config_schema.rs +312 -33
  129. package/src/rust/config/datetime_column_style.rs +0 -11
  130. package/src/rust/config/mod.rs +0 -4
  131. package/src/rust/config/options.rs +50 -0
  132. package/src/rust/config/workspace_config.rs +70 -24
  133. package/src/rust/custom_elements/debug_plugin.rs +1 -10
  134. package/src/rust/custom_elements/mod.rs +0 -3
  135. package/src/rust/custom_elements/viewer.rs +194 -115
  136. package/src/rust/js/plugin.rs +3 -87
  137. package/src/rust/lib.rs +19 -18
  138. package/src/rust/presentation/column_locator.rs +3 -7
  139. package/src/rust/presentation.rs +68 -38
  140. package/src/rust/queries/hosted_tables.rs +38 -0
  141. package/src/rust/queries/mod.rs +2 -0
  142. package/src/rust/queries/palette_set.rs +1 -1
  143. package/src/rust/queries/plugin_column_styles.rs +6 -5
  144. package/src/rust/renderer/activate.rs +1 -3
  145. package/src/rust/renderer/dispatch.rs +83 -22
  146. package/src/rust/renderer/limits.rs +24 -5
  147. package/src/rust/renderer/plugin_config.rs +201 -41
  148. package/src/rust/renderer/plugin_store.rs +2 -1
  149. package/src/rust/renderer/props.rs +7 -1
  150. package/src/rust/renderer.rs +23 -27
  151. package/src/rust/session/props.rs +8 -3
  152. package/src/rust/session/view_subscription.rs +19 -6
  153. package/src/rust/session.rs +189 -150
  154. package/src/rust/tasks/create_panel.rs +5 -2
  155. package/src/rust/tasks/dismiss_render_warning.rs +1 -1
  156. package/src/rust/tasks/eject.rs +1 -0
  157. package/src/rust/tasks/pipeline.rs +18 -67
  158. package/src/rust/tasks/presize_panels.rs +24 -1
  159. package/src/rust/tasks/restore_and_render.rs +0 -17
  160. package/src/rust/tasks/restore_panel.rs +129 -30
  161. package/src/rust/tasks/send_column_config.rs +4 -1
  162. package/src/rust/tasks/send_plugin_config.rs +1 -1
  163. package/src/rust/tasks/set_edit_mode.rs +1 -1
  164. package/src/rust/tasks/sync_update_panels.rs +147 -67
  165. package/src/rust/tasks/table_lifecycle.rs +7 -1
  166. package/src/rust/{components → ui}/containers/control_group.rs +25 -3
  167. package/src/rust/{components → ui}/containers/dropdown_menu.rs +5 -2
  168. package/src/rust/{components → ui}/containers/mod.rs +0 -4
  169. package/src/rust/{components → ui}/containers/scroll_panel.rs +0 -22
  170. package/src/rust/{components → ui}/containers/scroll_panel_item.rs +0 -13
  171. package/src/rust/{components → ui}/containers/select.rs +0 -6
  172. package/src/rust/{components → ui}/containers/sidebar.rs +5 -32
  173. package/src/rust/{components → ui}/containers/split_panel.rs +8 -80
  174. package/src/rust/{components → ui}/containers/tab_list.rs +15 -9
  175. package/src/rust/{components → ui}/context_menu.rs +8 -13
  176. package/src/rust/{presentation/drag_helpers.rs → ui/dragdrop.rs} +26 -31
  177. package/src/rust/{components → ui}/font_loader.rs +0 -26
  178. package/src/rust/{components → ui}/form/color_selector.rs +4 -2
  179. package/src/rust/{config/datetime_column_style/color_mode.rs → ui/form/intl_label.rs} +42 -42
  180. package/src/rust/{components → ui}/form/mirrored_textarea.rs +0 -53
  181. package/src/rust/{components → ui}/form/mod.rs +1 -10
  182. package/src/rust/{components → ui}/form/multi_stop_gradient_selector.rs +4 -5
  183. package/src/rust/{components → ui}/form/named_value_picker.rs +1 -6
  184. package/src/rust/{components → ui}/form/number_field.rs +1 -1
  185. package/src/rust/{components → ui}/form/number_range_field.rs +1 -1
  186. package/src/rust/{components → ui}/form/optional_field.rs +3 -1
  187. package/src/rust/{components → ui}/form/palette_selector.rs +4 -5
  188. package/src/rust/{components → ui}/form/select_enum_field.rs +2 -2
  189. package/src/rust/{components → ui}/form/select_value_field.rs +2 -2
  190. package/src/rust/ui/mod.rs +56 -0
  191. package/src/rust/ui/modal.rs +61 -0
  192. package/src/rust/{components → ui}/portal.rs +175 -79
  193. package/src/rust/{components/style/style_provider.rs → ui/style.rs} +4 -34
  194. package/src/rust/utils/completion.rs +0 -6
  195. package/src/rust/{config/css_scale.rs → utils/css.rs} +38 -35
  196. package/src/rust/utils/css_vars.rs +13 -0
  197. package/src/rust/utils/local_fonts.rs +395 -0
  198. package/src/rust/utils/mod.rs +4 -0
  199. package/src/rust/workspace.rs +47 -0
  200. package/src/svg/boolean-type.svg +3 -2
  201. package/src/svg/checkbox-checked-icon.svg +4 -7
  202. package/src/svg/checkbox-hover.svg +3 -3
  203. package/src/svg/checkbox-off.svg +2 -3
  204. package/src/svg/checkbox-on.svg +3 -3
  205. package/src/svg/checkbox-unchecked-icon.svg +3 -3
  206. package/src/svg/datagrid-select-column.svg +8 -15
  207. package/src/svg/datagrid-select-region.svg +8 -15
  208. package/src/svg/datagrid-select-row-tree.svg +7 -12
  209. package/src/svg/datagrid-select-row.svg +8 -13
  210. package/src/svg/date-type.svg +4 -23
  211. package/src/svg/drawer-tab-hover.svg +4 -6
  212. package/src/svg/drawer-tab-invert-hover.svg +5 -5
  213. package/src/svg/drawer-tab-invert.svg +3 -5
  214. package/src/svg/drawer-tab.svg +5 -5
  215. package/src/svg/mega-menu-icons-candlestick.svg +5 -37
  216. package/src/svg/mega-menu-icons-ohlc.svg +4 -39
  217. package/src/svg/mega-menu-icons-sunburst.svg +4 -38
  218. package/src/svg/mega-menu-icons-treemap.svg +3 -43
  219. package/src/svg/mega-menu-icons-y-scatter.svg +12 -72
  220. package/src/svg/new-panel-icon.svg +4 -0
  221. package/src/svg/number-type.svg +3 -3
  222. package/src/svg/radio-hover.svg +3 -3
  223. package/src/svg/radio-off.svg +3 -3
  224. package/src/svg/radio-on.svg +3 -3
  225. package/src/svg/status_ok.svg +3 -26
  226. package/src/svg/status_pending.svg +3 -0
  227. package/src/svg/string-type.svg +4 -8
  228. package/src/svg/theme-icon.svg +5 -5
  229. package/src/themes/blueprint.css +0 -57
  230. package/src/themes/botanical.css +0 -36
  231. package/src/themes/defaults.css +1 -24
  232. package/src/themes/dracula.css +0 -58
  233. package/src/themes/eggplant.css +0 -58
  234. package/src/themes/gruvbox-dark.css +0 -65
  235. package/src/themes/gruvbox.css +1 -39
  236. package/src/themes/icons.css +2 -8
  237. package/src/themes/intl/ar.css +198 -0
  238. package/src/themes/intl/de.css +63 -40
  239. package/src/themes/intl/es.css +58 -35
  240. package/src/themes/intl/fr.css +57 -34
  241. package/src/themes/intl/ja.css +59 -36
  242. package/src/themes/intl/pt.css +54 -31
  243. package/src/themes/intl/zh.css +65 -42
  244. package/src/themes/intl.css +34 -8
  245. package/src/themes/ledger.css +1 -59
  246. package/src/themes/monokai.css +0 -51
  247. package/src/themes/nord.css +0 -55
  248. package/src/themes/phosphor.css +0 -48
  249. package/src/themes/pro-dark.css +0 -37
  250. package/src/themes/pro.css +3 -23
  251. package/src/themes/solarized-dark.css +0 -31
  252. package/src/themes/solarized.css +1 -24
  253. package/src/themes/vaporwave.css +0 -44
  254. package/src/themes/velvet.css +0 -60
  255. package/src/ts/extensions.ts +0 -13
  256. package/src/ts/perspective-viewer.ts +2 -0
  257. package/src/ts/plugin.ts +9 -0
  258. package/src/ts/ts-rs/AddPanelOptions.ts +10 -0
  259. package/src/ts/ts-rs/{DatetimeColorMode.ts → GetViewMode.ts} +4 -1
  260. package/src/ts/ts-rs/GetViewOptions.ts +7 -0
  261. package/src/ts/ts-rs/RestoreOptions.ts +7 -1
  262. package/src/ts/ts-rs/RestoreWorkspaceOptions.ts +10 -0
  263. package/src/ts/ts-rs/ViewOnUpdateResp.ts +1 -1
  264. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +31 -20
  265. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +0 -1
  266. package/dist/esm/ts-rs/FormatMode.d.ts +0 -1
  267. package/dist/esm/ts-rs/StringColorMode.d.ts +0 -1
  268. package/src/css/plugin-settings-panel.css +0 -171
  269. package/src/rust/components/modal.rs +0 -177
  270. package/src/rust/components/string_column_style.rs +0 -121
  271. package/src/rust/config/string_column_style.rs +0 -135
  272. package/src/rust/custom_elements/copy_dropdown.rs +0 -219
  273. package/src/rust/custom_elements/export_dropdown.rs +0 -216
  274. package/src/rust/custom_elements/modal.rs +0 -299
  275. package/src/ts/ts-rs/FormatMode.ts +0 -3
  276. package/src/ts/ts-rs/StringColorMode.ts +0 -3
  277. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline0.js +0 -0
  278. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline1.js +0 -0
  279. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline2.js +0 -0
  280. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline3.js +0 -0
  281. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline4.js +0 -0
  282. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline5.js +0 -0
  283. /package/src/css/{containers → ui/containers}/scroll-panel.css +0 -0
  284. /package/src/css/{containers → ui/containers}/split-panel.css +0 -0
  285. /package/src/css/{dom → ui/dom}/checkbox.css +0 -0
  286. /package/src/css/{dom → ui/dom}/scrollbar.css +0 -0
  287. /package/src/css/{dom → ui/dom}/select.css +0 -0
  288. /package/src/css/{form → ui/form}/mirrored-textarea.css +0 -0
  289. /package/src/rust/components/{form → code_editor}/highlight.rs +0 -0
  290. /package/src/rust/{components → ui}/containers/sidebar_close_button.rs +0 -0
  291. /package/src/rust/{components → ui}/form/number_input.rs +0 -0
@@ -19,6 +19,7 @@ use std::future::Future;
19
19
  use std::rc::Rc;
20
20
 
21
21
  use perspective_client::View;
22
+ use perspective_client::proto::ViewDimensionsResp;
22
23
  use perspective_js::utils::{ApiResult, ResultTApiErrorExt};
23
24
  use wasm_bindgen::prelude::*;
24
25
  use web_sys::*;
@@ -44,25 +45,27 @@ impl Renderer {
44
45
  let _ = el.class_list().toggle_with_force("multi", !is_solo);
45
46
  }
46
47
 
47
- /// Stamp this panel's effective `theme` attribute.
48
48
  pub fn stamp_theme(&self, plugin: Option<&JsPerspectiveViewerPlugin>) {
49
+ let theme = self.theme();
49
50
  let active_plugin = self.active_plugin();
50
51
  if let Some(plugin) = plugin.or(active_plugin.as_ref()) {
51
- let theme_elem = plugin.unchecked_ref::<HtmlElement>();
52
- match self.theme() {
53
- Some(theme)
54
- if theme_elem.get_attribute("theme").as_deref() != Some(theme.as_str()) =>
55
- {
56
- let _ = theme_elem.set_attribute("theme", &theme);
57
- },
58
- Some(_) => {},
59
- None => {
60
- let _ = theme_elem.remove_attribute("theme");
61
- },
62
- }
52
+ stamp_theme_attr(plugin.unchecked_ref::<Element>(), theme.as_deref());
53
+ }
54
+
55
+ if let Some(tab) = self.tab_host() {
56
+ stamp_theme_attr(&tab, theme.as_deref());
63
57
  }
64
58
  }
65
59
 
60
+ fn tab_host(&self) -> Option<Element> {
61
+ let slot = format!("tab-{}", self.slot_name()?);
62
+ let viewer = self.plugin_data.borrow().viewer_elem.clone();
63
+ let children = viewer.children();
64
+ (0..children.length())
65
+ .filter_map(|i| children.item(i))
66
+ .find(|el| el.get_attribute("slot").as_deref() == Some(slot.as_str()))
67
+ }
68
+
66
69
  /// Restyle the active plugin and re-draw.
67
70
  pub async fn restyle_all(&self) -> ApiResult<JsValue> {
68
71
  self.render_task(|guard| async move {
@@ -78,8 +81,14 @@ impl Renderer {
78
81
  self.stamp_active(&plugin);
79
82
  plugin.restyle();
80
83
  *self.0.captured_theme.borrow_mut() = Some(stamped_theme);
81
- let mut limits =
82
- get_row_and_col_limits(&ctx.view, &meta, self.is_render_warning_enabled()).await?;
84
+ let dimensions = ctx.view.dimensions().await?;
85
+ let mut limits = get_row_and_col_limits(
86
+ &dimensions,
87
+ &ctx.view,
88
+ &meta,
89
+ self.is_render_warning_enabled(),
90
+ )
91
+ .await?;
83
92
 
84
93
  limits.is_update = true;
85
94
  plugin
@@ -215,6 +224,7 @@ impl Renderer {
215
224
  };
216
225
 
217
226
  self.stamp_active(&jsplugin);
227
+ self.0.presized_box.set(None);
218
228
  jsplugin.resize().await?;
219
229
  Ok(None)
220
230
  },
@@ -241,7 +251,33 @@ impl Renderer {
241
251
  self.stamp_active(&plugin);
242
252
  self.clear_presize()?;
243
253
  if plugin.capabilities().presize {
244
- return plugin.presize(width, height).await;
254
+ let Some(present) = plugin.presize(width, height).await? else {
255
+ return Ok(None);
256
+ };
257
+
258
+ self.0.presized_box.set(Some((width, height)));
259
+ let inline = js_sys::Reflect::get(&present, &"inline".into())
260
+ .ok()
261
+ .and_then(|v| v.as_bool())
262
+ .unwrap_or(false);
263
+
264
+ if inline {
265
+ self.0.presized_box.set(None);
266
+ }
267
+
268
+ let renderer = self.clone();
269
+ let wrapped = Closure::once_into_js(move || {
270
+ let filled = present
271
+ .call0(&JsValue::NULL)
272
+ .map(|v| v.as_bool() != Some(false))
273
+ .unwrap_or(false);
274
+
275
+ if !filled {
276
+ renderer.0.presized_box.set(None);
277
+ }
278
+ });
279
+
280
+ return Ok(Some(wrapped.unchecked_into::<js_sys::Function>()));
245
281
  }
246
282
 
247
283
  let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref();
@@ -278,6 +314,7 @@ impl Renderer {
278
314
  }
279
315
 
280
316
  result?;
317
+ self.0.presized_box.set(Some((width, height)));
281
318
  }
282
319
 
283
320
  Ok(None)
@@ -307,14 +344,16 @@ impl Renderer {
307
344
  pub async fn draw_fresh(&self, guard: &RenderGuard, view: FreshView) -> ApiResult<()> {
308
345
  let timer = self.render_timer();
309
346
  timer
310
- .capture_time(self.draw_view(guard, view.view(), false))
347
+ .capture_time(self.draw_view(guard, view.view(), false, None))
311
348
  .await
312
349
  }
313
350
 
314
351
  /// Repaint the ALREADY-BOUND `view` on the active plugin.
315
352
  pub async fn update_bound(&self, guard: &RenderGuard, view: &View) -> ApiResult<()> {
316
353
  let timer = self.render_timer();
317
- timer.capture_time(self.draw_view(guard, view, true)).await
354
+ timer
355
+ .capture_time(self.draw_view(guard, view, true, None))
356
+ .await
318
357
  }
319
358
 
320
359
  /// Mint the [`FreshView`] full-draw witness for a plugin that has never
@@ -363,7 +402,7 @@ impl Renderer {
363
402
  /// Redraw an already-bound view, debounced.
364
403
  pub async fn update_lazy(
365
404
  &self,
366
- view: impl Future<Output = ApiResult<Option<View>>>,
405
+ view: impl Future<Output = ApiResult<Option<(View, Option<ViewDimensionsResp>)>>>,
367
406
  ) -> ApiResult<()> {
368
407
  let timer = self.render_timer();
369
408
  self.draw_lock()
@@ -382,14 +421,14 @@ impl Renderer {
382
421
  }
383
422
 
384
423
  self.mount_active_plugin()?;
385
- if let Some(view) = view.await? {
424
+ if let Some((view, dimensions)) = view.await? {
386
425
  let _pin = self
387
426
  .cached_context()
388
427
  .map(|ctx| self.pin_context(&guard, ctx));
389
428
 
390
429
  let timer = self.render_timer();
391
430
  timer
392
- .capture_time(self.draw_view(&guard, &view, true))
431
+ .capture_time(self.draw_view(&guard, &view, true, dimensions))
393
432
  .await
394
433
  } else {
395
434
  tracing::debug!("Render skipped, no `View` attached");
@@ -399,11 +438,14 @@ impl Renderer {
399
438
  .await
400
439
  }
401
440
 
441
+ /// `dimensions` is the caller's already-fetched view dimensions, or
442
+ /// `None` to fetch here.
402
443
  async fn draw_view(
403
444
  &self,
404
445
  _guard: &RenderGuard,
405
446
  view: &perspective_client::View,
406
447
  is_update: bool,
448
+ dimensions: Option<ViewDimensionsResp>,
407
449
  ) -> ApiResult<()> {
408
450
  debug_assert!(
409
451
  !is_update || self.cached_context().is_none() || self.render_context().is_some(),
@@ -420,8 +462,14 @@ impl Renderer {
420
462
  };
421
463
 
422
464
  let meta = self.metadata();
465
+ let dimensions = match dimensions {
466
+ Some(dimensions) => dimensions,
467
+ None => view.dimensions().await?,
468
+ };
469
+
423
470
  let mut limits =
424
- get_row_and_col_limits(view, &meta, self.is_render_warning_enabled()).await?;
471
+ get_row_and_col_limits(&dimensions, view, &meta, self.is_render_warning_enabled())
472
+ .await?;
425
473
 
426
474
  limits.is_update = is_update;
427
475
  if let Some(cb) = self.0.on_render_limits_changed.borrow().as_ref() {
@@ -440,6 +488,7 @@ impl Renderer {
440
488
  }
441
489
 
442
490
  self.0.data_stale.set(false);
491
+ self.0.presized_box.set(None);
443
492
  let result = if is_update {
444
493
  let task = plugin.update(view.clone().into(), limits.max_cols, limits.max_rows, false);
445
494
  activate_plugin(_guard, &viewer_elem, &plugin, slot.as_deref(), task).await
@@ -499,3 +548,15 @@ impl Drop for PresizePendingGuard {
499
548
  cell.set(cell.get() - 1);
500
549
  }
501
550
  }
551
+
552
+ fn stamp_theme_attr(el: &Element, theme: Option<&str>) {
553
+ match theme {
554
+ Some(theme) if el.get_attribute("theme").as_deref() != Some(theme) => {
555
+ let _ = el.set_attribute("theme", theme);
556
+ },
557
+ Some(_) => {},
558
+ None => {
559
+ let _ = el.remove_attribute("theme");
560
+ },
561
+ }
562
+ }
@@ -10,6 +10,7 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ use perspective_client::proto::ViewDimensionsResp;
13
14
  use perspective_js::utils::ApiError;
14
15
 
15
16
  use crate::config::PluginStaticConfig;
@@ -31,16 +32,34 @@ pub struct RenderLimits {
31
32
  pub max_rows: Option<usize>,
32
33
  }
33
34
 
34
- /// Compute the row/column caps for `view` against the plugin's static
35
- /// limits. `render_warning` is the renderer-state flag controlling
36
- /// whether the warning is currently armed; when `false`, an oversized
37
- /// view renders uncapped (the user has dismissed the warning).
35
+ impl RenderLimits {
36
+ /// The column cap is in effect (fewer columns render than the view has).
37
+ pub fn is_col_capped(&self) -> bool {
38
+ self.max_cols.is_some_and(|x| x < self.num_cols)
39
+ }
40
+
41
+ /// The row cap is in effect (fewer rows render than the view has).
42
+ pub fn is_row_capped(&self) -> bool {
43
+ self.max_rows.is_some_and(|x| x < self.num_rows)
44
+ }
45
+
46
+ /// Either cap is in effect — exactly when the render warning shows.
47
+ pub fn is_capped(&self) -> bool {
48
+ self.is_col_capped() || self.is_row_capped()
49
+ }
50
+ }
51
+
52
+ /// Compute the row/column caps for `view` from its already-fetched
53
+ /// `dimensions` against the plugin's static limits. `render_warning` is the
54
+ /// renderer-state flag controlling whether the warning is currently armed;
55
+ /// when `false`, an oversized view renders uncapped (the user has dismissed
56
+ /// the warning).
38
57
  pub async fn get_row_and_col_limits(
58
+ dimensions: &ViewDimensionsResp,
39
59
  view: &perspective_client::View,
40
60
  config: &PluginStaticConfig,
41
61
  render_warning: bool,
42
62
  ) -> Result<RenderLimits, ApiError> {
43
- let dimensions = view.dimensions().await?;
44
63
  let num_cols = dimensions.num_view_columns as usize;
45
64
  let num_rows = dimensions.num_view_rows as usize;
46
65
  match (config.max_columns, render_warning) {
@@ -26,6 +26,7 @@ use super::Renderer;
26
26
  use crate::config::*;
27
27
  use crate::queries::resolve_abs_max;
28
28
  use crate::session::Session;
29
+ use crate::utils::{CssKind, CssLiteralUse, parse_var_ref, resolve_css_refs};
29
30
 
30
31
  /// A per-column config map. Each inner [`serde_json::Map`] is a flat collection
31
32
  /// of plugin-defined JSON keys whose shape is dictated by the active plugin's
@@ -246,10 +247,16 @@ impl Renderer {
246
247
  {
247
248
  let active = schema.active_keys();
248
249
  cfg.retain(|k, _| active.contains(k));
249
- let errors = normalize_css_values(&schema, &mut cfg);
250
+ let errors = normalize_values(&schema, &mut cfg);
250
251
  if let Some((key, error)) = errors.first() {
252
+ let ty = session
253
+ .metadata()
254
+ .get_column_view_type(&col)
255
+ .map(|t| format!(" for a {t} column"))
256
+ .unwrap_or_default();
257
+
251
258
  return Err(ApiError::from(JsValue::from_str(&format!(
252
- "Invalid `columns_config[\"{col}\"].{key}`: {error}"
259
+ "Invalid `columns_config[\"{col}\"].{key}`{ty}: {error}"
253
260
  ))));
254
261
  }
255
262
 
@@ -304,7 +311,7 @@ impl Renderer {
304
311
  &column_name,
305
312
  current_value.as_ref(),
306
313
  ) {
307
- for (key, error) in normalize_css_values(&schema, &mut update.value) {
314
+ for (key, error) in normalize_values(&schema, &mut update.value) {
308
315
  tracing::error!("Dropping `{column_name}`.`{key}`: {error}");
309
316
  }
310
317
 
@@ -328,16 +335,25 @@ impl Renderer {
328
335
  entry.clone()
329
336
  };
330
337
 
331
- let active = self
338
+ // The schema AFTER the update decides which keys survive and, for a
339
+ // key whose control kind follows another key (`fg_color` reads as a
340
+ // color, gradient or palette depending on `fg_mode`), whether the
341
+ // value still parses under its new kind. A value that no longer
342
+ // does is dropped here, so `save()` never emits a config that
343
+ // `restore()` would reject.
344
+ let after = self
332
345
  .query_column_config_schema(view_config, session, &column_name, Some(&next))
333
- .ok()
334
- .map(|schema| schema.active_keys());
346
+ .ok();
335
347
 
336
348
  let mut st = self.borrow_mut();
337
349
  let bucket = st.plugin_states.entry(n).or_default();
338
350
  if let Some(entry) = bucket.columns.get_mut(&column_name) {
339
- if let Some(active) = &active {
351
+ if let Some(schema) = &after {
352
+ let active = schema.active_keys();
340
353
  entry.retain(|k, _| active.contains(k));
354
+ for (key, error) in normalize_values(schema, entry) {
355
+ tracing::warn!("Dropping `{column_name}`.`{key}` after a mode change: {error}");
356
+ }
341
357
  }
342
358
 
343
359
  if entry.is_empty() {
@@ -370,15 +386,18 @@ impl Renderer {
370
386
  }
371
387
  }
372
388
 
373
- /// Synchronously query the active plugin's
374
- /// [`ColumnConfigSchema`] used to gate plugin-config strip logic.
389
+ /// Synchronously query the active plugin's [`ColumnConfigSchema`] as it
390
+ /// applies to `current_value`, the plugin-config state the schema should
391
+ /// describe.
375
392
  fn query_plugin_config_schema(
376
393
  &self,
377
394
  view_config: &ViewConfig,
395
+ current_value: Option<&serde_json::Map<String, serde_json::Value>>,
378
396
  ) -> ApiResult<ColumnConfigSchema> {
379
397
  let plugin = self.ensure_plugin_selected()?;
380
398
  let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
381
- let raw = plugin._plugin_config_schema(&view_config_js)?;
399
+ let current_js = JsValue::from_serde_ext(&current_value).unwrap_or(JsValue::NULL);
400
+ let raw = plugin._plugin_config_schema(&view_config_js, &current_js)?;
382
401
  serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
383
402
  .map(|schema| schema.canonicalize())
384
403
  .map_err(|e| e.into())
@@ -433,12 +452,9 @@ impl Renderer {
433
452
  .map_err(|e| e.into())
434
453
  }
435
454
 
436
- /// Wholesale update the active plugin's plugin-level config map.
437
- /// Entries whose value equals the schema-declared default are
438
- /// treated as "reset this key" — the corresponding bucket entry
439
- /// is cleared rather than the default being stored literally.
440
- /// Keys absent from the incoming map are left alone (merge
441
- /// semantics for the non-default subset).
455
+ /// Merge an update into the active plugin's plugin-level config map,
456
+ /// clearing entries equal to the schema default and dropping keys the
457
+ /// resulting schema no longer advertises.
442
458
  pub fn update_plugin_config(
443
459
  &self,
444
460
  view_config: &ViewConfig,
@@ -448,28 +464,42 @@ impl Renderer {
448
464
  return Ok(false);
449
465
  };
450
466
 
451
- let schema = self.query_plugin_config_schema(view_config).ok();
452
- let mut st = self.borrow_mut();
453
- let bucket = st.plugin_states.entry(n).or_default();
454
467
  match update {
455
468
  OptionalUpdate::SetDefault => {
469
+ let mut st = self.borrow_mut();
470
+ let bucket = st.plugin_states.entry(n).or_default();
456
471
  let changed = !bucket.plugin.is_empty();
457
472
  bucket.plugin.clear();
458
473
  Ok(changed)
459
474
  },
460
475
  OptionalUpdate::Missing => Ok(false),
461
476
  OptionalUpdate::Update(mut map) => {
462
- let mut changed = false;
477
+ let mut merged = self.get_plugin_config();
478
+ for (k, v) in &map {
479
+ merged.insert(k.clone(), v.clone());
480
+ }
481
+
482
+ let schema = self
483
+ .query_plugin_config_schema(view_config, Some(&merged))
484
+ .ok();
485
+
486
+ let mut active = None;
463
487
  if let Some(s) = &schema {
464
- let active = s.active_keys();
465
- map.retain(|k, _| active.contains(k));
466
- let errors = normalize_css_values(s, &mut map);
488
+ let keys = s.active_keys();
489
+ map.retain(|k, _| keys.contains(k));
490
+ active = Some(keys);
491
+ let errors = normalize_values(s, &mut map);
467
492
  if let Some((key, error)) = errors.first() {
468
493
  return Err(ApiError::from(JsValue::from_str(&format!(
469
494
  "Invalid `plugin_config.{key}`: {error}"
470
495
  ))));
471
496
  }
497
+ }
472
498
 
499
+ let mut st = self.borrow_mut();
500
+ let bucket = st.plugin_states.entry(n).or_default();
501
+ let mut changed = false;
502
+ if let Some(s) = &schema {
473
503
  let leaves = s.leaf_fields();
474
504
  map.retain(|key, value| {
475
505
  let is_default = leaves
@@ -493,12 +523,19 @@ impl Renderer {
493
523
  }
494
524
  }
495
525
 
526
+ if let Some(active) = active {
527
+ let before = bucket.plugin.len();
528
+ bucket.plugin.retain(|k, _| active.contains(k));
529
+ changed |= bucket.plugin.len() != before;
530
+ }
531
+
496
532
  Ok(changed)
497
533
  },
498
534
  }
499
535
  }
500
536
 
501
- /// Apply a single schema-field update from the plugin-settings UI
537
+ /// Apply a single schema-field update from the plugin-settings UI,
538
+ /// dropping keys the resulting schema no longer advertises.
502
539
  pub fn update_plugin_config_field(
503
540
  &self,
504
541
  view_config: &ViewConfig,
@@ -508,12 +545,28 @@ impl Renderer {
508
545
  return false;
509
546
  };
510
547
 
511
- if let Ok(schema) = self.query_plugin_config_schema(view_config) {
512
- for (key, error) in normalize_css_values(&schema, &mut update.value) {
548
+ let mut next = self.get_plugin_config();
549
+ for k in &update.keys {
550
+ match update.value.get(k) {
551
+ Some(v) => {
552
+ next.insert(k.clone(), v.clone());
553
+ },
554
+ None => {
555
+ next.remove(k);
556
+ },
557
+ }
558
+ }
559
+
560
+ let schema = self
561
+ .query_plugin_config_schema(view_config, Some(&next))
562
+ .ok();
563
+
564
+ if let Some(schema) = &schema {
565
+ for (key, error) in normalize_values(schema, &mut update.value) {
513
566
  tracing::error!("Dropping `plugin_config`.`{key}`: {error}");
514
567
  }
515
568
 
516
- strip_default_values(&schema, &mut update.value);
569
+ strip_default_values(schema, &mut update.value);
517
570
  }
518
571
 
519
572
  let mut st = self.borrow_mut();
@@ -531,28 +584,48 @@ impl Renderer {
531
584
  }
532
585
  }
533
586
 
587
+ if let Some(schema) = &schema {
588
+ let active = schema.active_keys();
589
+ let before = bucket.plugin.len();
590
+ bucket.plugin.retain(|k, _| active.contains(k));
591
+ changed |= bucket.plugin.len() != before;
592
+ }
593
+
534
594
  changed
535
595
  }
536
596
  }
537
597
 
538
- fn normalize_css_values(
598
+ /// Canonicalize every CSS-valued key of `map` and check every `Enum`-valued
599
+ /// key against its variants, removing and reporting the keys that fail.
600
+ /// The schema is the one queried for `map`'s own values, so an `Enum`'s
601
+ /// variants are exactly the values the column's type accepts.
602
+ fn normalize_values(
539
603
  schema: &ColumnConfigSchema,
540
604
  map: &mut serde_json::Map<String, serde_json::Value>,
541
605
  ) -> Vec<(String, String)> {
542
606
  let mut errors = vec![];
543
607
  for (key, value) in map.iter_mut() {
544
- let Some(kind) = schema.css_kind_of(key) else {
545
- continue;
546
- };
547
-
548
- let canonical = match value.as_str() {
549
- Some(src) => kind.canonicalize(src),
550
- None => Err(format!("expected a CSS string, got `{value}`")),
551
- };
608
+ if let Some(kind) = schema.css_kind_of(key) {
609
+ let canonical = match value.as_str() {
610
+ Some(src) => kind.canonicalize(src),
611
+ None => Err(format!("expected a CSS string, got `{value}`")),
612
+ };
552
613
 
553
- match canonical {
554
- Ok(canonical) => *value = Value::String(canonical),
555
- Err(error) => errors.push((key.clone(), error)),
614
+ match canonical {
615
+ Ok(canonical) => *value = Value::String(canonical),
616
+ Err(error) => errors.push((key.clone(), error)),
617
+ }
618
+ } else if let Some(variants) = schema.enum_variants_of(key) {
619
+ let accepted = variants.iter().any(|v| value.as_str() == Some(&v.value));
620
+ if !accepted {
621
+ let expected = variants
622
+ .iter()
623
+ .map(|v| format!("`{}`", v.value))
624
+ .collect::<Vec<_>>()
625
+ .join(", ");
626
+
627
+ errors.push((key.clone(), format!("{value} is not one of {expected}")));
628
+ }
556
629
  }
557
630
  }
558
631
 
@@ -579,6 +652,21 @@ fn matches_declared_default(spec: &ControlSpec, key: &str, value: &Value) -> boo
579
652
  match spec {
580
653
  ControlSpec::Enum {
581
654
  key: k, default, ..
655
+ }
656
+ | ControlSpec::Font {
657
+ key: k, default, ..
658
+ } if k == key => value.as_str() == Some(default.as_str()),
659
+ ControlSpec::Font {
660
+ size: Some(size), ..
661
+ } if size.key == key => value.as_f64() == Some(size.default),
662
+ ControlSpec::Font { bold, italic, .. } => [bold, italic]
663
+ .into_iter()
664
+ .flatten()
665
+ .any(|toggle| toggle.key == key && value.as_bool() == Some(toggle.default)),
666
+ ControlSpec::Alignment {
667
+ key: k,
668
+ default: Some(default),
669
+ ..
582
670
  } if k == key => value.as_str() == Some(default.as_str()),
583
671
  ControlSpec::Bool {
584
672
  key: k, default, ..
@@ -702,6 +790,35 @@ mod tests {
702
790
  ));
703
791
  }
704
792
 
793
+ #[test]
794
+ fn alignment_strips_only_a_declared_default() {
795
+ let schema = ColumnConfigSchema {
796
+ fields: vec![
797
+ ControlSpec::Alignment {
798
+ key: "align".to_owned(),
799
+ default: None,
800
+ corners: false,
801
+ },
802
+ ControlSpec::Alignment {
803
+ key: "legend_anchor".to_owned(),
804
+ default: Some(Alignment::TopRight),
805
+ corners: true,
806
+ },
807
+ ],
808
+ };
809
+
810
+ let mut map = json!({ "align": "center", "legend_anchor": "top-right" })
811
+ .as_object()
812
+ .unwrap()
813
+ .clone();
814
+
815
+ strip_default_values(&schema, &mut map);
816
+ assert_eq!(
817
+ map,
818
+ json!({ "align": "center" }).as_object().unwrap().clone()
819
+ );
820
+ }
821
+
705
822
  #[test]
706
823
  fn strip_default_values_sees_through_groups() {
707
824
  let leaves = vec![
@@ -750,7 +867,50 @@ mod tests {
750
867
  }
751
868
 
752
869
  #[test]
753
- fn normalize_css_values_canonicalizes_and_reports() {
870
+ fn normalize_values_rejects_enum_values_outside_the_variants() {
871
+ let schema = ColumnConfigSchema {
872
+ fields: vec![ControlSpec::Group {
873
+ key: "color".to_owned(),
874
+ fields: vec![ControlSpec::Enum {
875
+ key: "fg_mode".to_owned(),
876
+ default: "color".to_owned(),
877
+ variants: vec![
878
+ EnumVariant {
879
+ value: "disabled".to_owned(),
880
+ label: None,
881
+ },
882
+ EnumVariant {
883
+ value: "color".to_owned(),
884
+ label: None,
885
+ },
886
+ ],
887
+ }],
888
+ }],
889
+ };
890
+
891
+ let mut map = json!({ "fg_mode": "color", "other": "series" })
892
+ .as_object()
893
+ .unwrap()
894
+ .clone();
895
+
896
+ assert!(normalize_values(&schema, &mut map).is_empty());
897
+ assert_eq!(map.len(), 2);
898
+
899
+ let mut map = json!({ "fg_mode": "series" }).as_object().unwrap().clone();
900
+ let errors = normalize_values(&schema, &mut map);
901
+ assert_eq!(errors.len(), 1);
902
+ assert_eq!(errors[0].0, "fg_mode");
903
+ assert_eq!(errors[0].1, "\"series\" is not one of `disabled`, `color`");
904
+ assert!(map.is_empty());
905
+
906
+ let mut map = json!({ "fg_mode": 3 }).as_object().unwrap().clone();
907
+ let errors = normalize_values(&schema, &mut map);
908
+ assert_eq!(errors.len(), 1);
909
+ assert!(map.is_empty());
910
+ }
911
+
912
+ #[test]
913
+ fn normalize_values_canonicalizes_and_reports() {
754
914
  let schema = ColumnConfigSchema {
755
915
  fields: vec![
756
916
  ControlSpec::Color {
@@ -785,7 +945,7 @@ mod tests {
785
945
  .unwrap()
786
946
  .clone();
787
947
 
788
- let errors = normalize_css_values(&schema, &mut map);
948
+ let errors = normalize_values(&schema, &mut map);
789
949
  assert_eq!(errors.len(), 1);
790
950
  assert_eq!(errors[0].0, "gradient");
791
951
  assert_eq!(
@@ -26,7 +26,8 @@ pub struct PluginStore {
26
26
 
27
27
  impl PluginStore {
28
28
  fn init_lazy(&mut self) {
29
- self.plugins = Some(PLUGIN_REGISTRY.create_plugins());
29
+ let plugins = PLUGIN_REGISTRY.create_plugins();
30
+ self.plugins = Some(plugins);
30
31
  self.plugin_configs = Some(PLUGIN_REGISTRY.plugin_configs());
31
32
  self.plugin_records = Some(PLUGIN_REGISTRY.available_plugin_names_by_category());
32
33
  }
@@ -14,6 +14,7 @@ use std::rc::Rc;
14
14
 
15
15
  use crate::config::PluginStaticConfig;
16
16
  use crate::renderer::limits::RenderLimits;
17
+ use crate::renderer::plugin_config::ColumnConfigMap;
17
18
  use crate::utils::PtrEqRc;
18
19
 
19
20
  /// Value-semantic snapshot of the renderer state read by components.
@@ -30,7 +31,8 @@ pub struct RendererProps {
30
31
  /// internal `metadata` — `Rc::ptr_eq` works.
31
32
  pub config: Rc<PluginStaticConfig>,
32
33
 
33
- /// Most recently emitted render limits, if any.
34
+ /// The most recently emitted render limits, only while a cap is in
35
+ /// effect.
34
36
  pub render_limits: Option<RenderLimits>,
35
37
 
36
38
  /// Names of all registered plugins, in registration order.
@@ -49,6 +51,10 @@ pub struct RendererProps {
49
51
  /// the interior-mutable handle, no manual PubSub subscription, no
50
52
  /// race window between `plugins_idx` swap and the schema query.
51
53
  pub plugin_config: PtrEqRc<serde_json::Map<String, serde_json::Value>>,
54
+
55
+ /// Snapshot of the active plugin's `columns_config` bucket, re-read by
56
+ /// `Renderer::to_props` on `columns_config_changed` and `plugin_changed`.
57
+ pub columns_config: PtrEqRc<ColumnConfigMap>,
52
58
  }
53
59
 
54
60
  impl RendererProps {