@perspective-dev/viewer 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/dist/cdn/perspective-viewer.js +8 -8
  2. package/dist/cdn/perspective-viewer.js.map +3 -3
  3. package/dist/css/blueprint.css +1 -0
  4. package/dist/css/botanical.css +1 -1
  5. package/dist/css/dracula.css +1 -1
  6. package/dist/css/eggplant.css +1 -0
  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 -0
  19. package/dist/css/monokai.css +1 -1
  20. package/dist/css/nord.css +1 -0
  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 -0
  29. package/dist/docs/perspective-docs.json +1 -1
  30. package/dist/esm/column-format.d.ts +16 -3
  31. package/dist/esm/extensions.d.ts +0 -5
  32. package/dist/esm/perspective-viewer.inline.js +7 -7
  33. package/dist/esm/perspective-viewer.inline.js.map +3 -3
  34. package/dist/esm/perspective-viewer.js +8 -8
  35. package/dist/esm/perspective-viewer.js.map +3 -3
  36. package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +1 -1
  37. package/dist/wasm/perspective-viewer.d.ts +62 -73
  38. package/dist/wasm/perspective-viewer.js +230 -238
  39. package/dist/wasm/perspective-viewer.wasm +0 -0
  40. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -28
  41. package/package.json +7 -7
  42. package/src/css/_context_menu.css +1 -1
  43. package/src/css/_dropdown_menu.css +1 -1
  44. package/src/css/_viewer_bundle.css +7 -8
  45. package/src/css/column-dropdown.css +0 -3
  46. package/src/css/column-selector.css +8 -5
  47. package/src/css/column-settings-panel.css +0 -206
  48. package/src/css/column-style.css +95 -0
  49. package/src/css/config-selector.css +18 -10
  50. package/src/css/filter-dropdown.css +5 -5
  51. package/src/css/form/debug.css +28 -4
  52. package/src/css/function-dropdown.css +5 -5
  53. package/src/css/panel-tab.css +23 -0
  54. package/src/css/plugin-selector.css +6 -5
  55. package/src/css/render-warning.css +1 -1
  56. package/src/css/status-bar.css +52 -21
  57. package/src/css/type-icon.css +1 -3
  58. package/src/css/{containers → ui/containers}/context-menu.css +22 -16
  59. package/src/css/ui/containers/dropdown-menu-items.css +66 -0
  60. package/src/css/{containers → ui/containers}/dropdown-menu.css +8 -53
  61. package/src/css/{containers → ui/containers}/tabs.css +13 -3
  62. package/src/css/{dom → ui/dom}/select.css +19 -8
  63. package/src/css/viewer.css +32 -31
  64. package/src/rust/agent/tools.rs +5 -2
  65. package/src/rust/components/chat_panel.rs +1 -1
  66. package/src/rust/components/{form/code_editor.rs → code_editor.rs} +4 -2
  67. package/src/rust/components/column_dropdown.rs +2 -2
  68. package/src/rust/components/column_selector/active_column.rs +15 -9
  69. package/src/rust/components/column_selector/aggregate_selector.rs +1 -1
  70. package/src/rust/components/column_selector/config_selector.rs +2 -2
  71. package/src/rust/components/column_selector/filter_column.rs +8 -5
  72. package/src/rust/components/column_selector/inactive_column.rs +4 -2
  73. package/src/rust/components/column_selector/pivot_column.rs +4 -3
  74. package/src/rust/components/column_selector/sort_column.rs +7 -4
  75. package/src/rust/components/column_selector.rs +3 -3
  76. package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +1 -1
  77. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +15 -17
  78. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +1 -1
  79. package/src/rust/components/column_settings_sidebar/style_tab.rs +296 -238
  80. package/src/rust/components/column_settings_sidebar.rs +3 -4
  81. package/src/rust/components/copy_dropdown.rs +1 -1
  82. package/src/rust/components/datetime_column_style/custom.rs +1 -3
  83. package/src/rust/components/datetime_column_style/simple.rs +16 -6
  84. package/src/rust/components/datetime_column_style.rs +51 -12
  85. package/src/rust/components/{form/debug.rs → debug_panel.rs} +5 -5
  86. package/src/rust/components/{containers/dragdrop_list.rs → dragdrop_list.rs} +2 -10
  87. package/src/rust/components/export_dropdown.rs +1 -1
  88. package/src/rust/components/expression_editor.rs +1 -1
  89. package/src/rust/components/filter_dropdown.rs +2 -2
  90. package/src/rust/components/function_dropdown.rs +2 -2
  91. package/src/rust/components/main_panel/frame_theme.rs +3 -59
  92. package/src/rust/components/main_panel/reconcile.rs +9 -4
  93. package/src/rust/components/main_panel/render.rs +62 -11
  94. package/src/rust/components/main_panel/update.rs +39 -10
  95. package/src/rust/components/main_panel.rs +26 -2
  96. package/src/rust/components/mod.rs +5 -7
  97. package/src/rust/components/new_panel_menu.rs +97 -0
  98. package/src/rust/components/number_series_style.rs +2 -3
  99. package/src/rust/components/panel_menu.rs +61 -87
  100. package/src/rust/components/panel_tab.rs +100 -43
  101. package/src/rust/components/plugin_tab.rs +125 -87
  102. package/src/rust/components/render_warning.rs +2 -2
  103. package/src/rust/components/{status_bar_counter.rs → rows_counter.rs} +10 -10
  104. package/src/rust/components/settings_panel.rs +3 -2
  105. package/src/rust/components/status_bar.rs +113 -20
  106. package/src/rust/components/status_indicator.rs +5 -14
  107. package/src/rust/components/string_column_style.rs +1 -2
  108. package/src/rust/components/{style/mod.rs → style.rs} +32 -15
  109. package/src/rust/components/style_controls/number_string_format/digits_section.rs +25 -15
  110. package/src/rust/components/style_controls/number_string_format/misc_section.rs +7 -3
  111. package/src/rust/components/style_controls/number_string_format/style_section.rs +2 -1
  112. package/src/rust/components/style_controls/number_string_format/types.rs +24 -0
  113. package/src/rust/components/style_controls/number_string_format.rs +37 -14
  114. package/src/rust/components/viewer/msg.rs +43 -30
  115. package/src/rust/components/viewer/panels.rs +50 -19
  116. package/src/rust/components/viewer/render.rs +15 -13
  117. package/src/rust/components/viewer/settings.rs +137 -68
  118. package/src/rust/components/viewer/snapshots.rs +5 -10
  119. package/src/rust/components/viewer/wiring.rs +1 -21
  120. package/src/rust/components/viewer.rs +15 -10
  121. package/src/rust/components/window_editor.rs +8 -9
  122. package/src/rust/config/column_config_schema.rs +358 -52
  123. package/src/rust/config/datetime_column_style.rs +4 -21
  124. package/src/rust/config/mod.rs +0 -2
  125. package/src/rust/config/number_string_format.rs +211 -18
  126. package/src/rust/config/workspace_config.rs +2 -1
  127. package/src/rust/custom_elements/debug_plugin.rs +1 -10
  128. package/src/rust/custom_elements/mod.rs +0 -3
  129. package/src/rust/custom_elements/viewer.rs +0 -18
  130. package/src/rust/js/plugin.rs +0 -86
  131. package/src/rust/lib.rs +12 -15
  132. package/src/rust/presentation/column_locator.rs +3 -7
  133. package/src/rust/presentation.rs +85 -38
  134. package/src/rust/queries/hosted_tables.rs +38 -0
  135. package/src/rust/queries/mod.rs +2 -0
  136. package/src/rust/queries/palette_set.rs +1 -1
  137. package/src/rust/queries/plugin_column_styles.rs +2 -2
  138. package/src/rust/renderer/activate.rs +1 -3
  139. package/src/rust/renderer/dispatch.rs +117 -30
  140. package/src/rust/renderer/limits.rs +24 -5
  141. package/src/rust/renderer/plugin_config.rs +56 -8
  142. package/src/rust/renderer/props.rs +2 -1
  143. package/src/rust/renderer.rs +14 -27
  144. package/src/rust/session/props.rs +4 -3
  145. package/src/rust/session/view_subscription.rs +19 -6
  146. package/src/rust/session.rs +38 -70
  147. package/src/rust/tasks/create_panel.rs +1 -1
  148. package/src/rust/tasks/dismiss_render_warning.rs +1 -1
  149. package/src/rust/tasks/eject.rs +1 -0
  150. package/src/rust/tasks/pipeline.rs +0 -60
  151. package/src/rust/tasks/presize_panels.rs +24 -1
  152. package/src/rust/tasks/restore_and_render.rs +0 -17
  153. package/src/rust/tasks/send_column_config.rs +1 -1
  154. package/src/rust/tasks/send_plugin_config.rs +1 -1
  155. package/src/rust/tasks/set_edit_mode.rs +1 -1
  156. package/src/rust/ui/containers/control_group.rs +113 -0
  157. package/src/rust/{components → ui}/containers/dropdown_menu.rs +5 -2
  158. package/src/rust/{components → ui}/containers/mod.rs +1 -4
  159. package/src/rust/{components → ui}/containers/scroll_panel.rs +0 -22
  160. package/src/rust/{components → ui}/containers/scroll_panel_item.rs +0 -13
  161. package/src/rust/{components → ui}/containers/select.rs +8 -8
  162. package/src/rust/{components → ui}/containers/sidebar.rs +5 -32
  163. package/src/rust/{components → ui}/containers/split_panel.rs +8 -80
  164. package/src/rust/{components → ui}/containers/tab_list.rs +15 -9
  165. package/src/rust/{components → ui}/context_menu.rs +8 -13
  166. package/src/rust/{presentation/drag_helpers.rs → ui/dragdrop.rs} +26 -31
  167. package/src/rust/{components → ui}/font_loader.rs +0 -26
  168. package/src/rust/{components → ui}/form/color_selector.rs +4 -2
  169. package/src/rust/ui/form/intl_label.rs +81 -0
  170. package/src/rust/{components → ui}/form/mirrored_textarea.rs +0 -53
  171. package/src/rust/{components → ui}/form/mod.rs +1 -10
  172. package/src/rust/{components → ui}/form/multi_stop_gradient_selector.rs +4 -5
  173. package/src/rust/{components → ui}/form/named_value_picker.rs +1 -6
  174. package/src/rust/{components → ui}/form/number_field.rs +1 -1
  175. package/src/rust/{components → ui}/form/number_range_field.rs +1 -1
  176. package/src/rust/{components → ui}/form/optional_field.rs +3 -1
  177. package/src/rust/{components → ui}/form/palette_selector.rs +4 -5
  178. package/src/rust/{components → ui}/form/select_enum_field.rs +2 -2
  179. package/src/rust/{components → ui}/form/select_value_field.rs +2 -2
  180. package/src/rust/ui/mod.rs +56 -0
  181. package/src/rust/ui/modal.rs +61 -0
  182. package/src/rust/{components → ui}/portal.rs +174 -79
  183. package/src/rust/{components/style/style_provider.rs → ui/style.rs} +4 -34
  184. package/src/rust/utils/completion.rs +0 -6
  185. package/src/rust/{config/css_scale.rs → utils/css.rs} +38 -35
  186. package/src/rust/utils/mod.rs +2 -0
  187. package/src/rust/workspace.rs +35 -0
  188. package/src/svg/boolean-type.svg +3 -2
  189. package/src/svg/checkbox-checked-icon.svg +4 -7
  190. package/src/svg/checkbox-hover.svg +3 -3
  191. package/src/svg/checkbox-off.svg +2 -3
  192. package/src/svg/checkbox-on.svg +3 -3
  193. package/src/svg/checkbox-unchecked-icon.svg +3 -3
  194. package/src/svg/datagrid-select-column.svg +8 -15
  195. package/src/svg/datagrid-select-region.svg +8 -15
  196. package/src/svg/datagrid-select-row-tree.svg +7 -12
  197. package/src/svg/datagrid-select-row.svg +8 -13
  198. package/src/svg/date-type.svg +4 -23
  199. package/src/svg/drawer-tab-hover.svg +4 -6
  200. package/src/svg/drawer-tab-invert-hover.svg +5 -5
  201. package/src/svg/drawer-tab-invert.svg +3 -5
  202. package/src/svg/drawer-tab.svg +5 -5
  203. package/src/svg/mega-menu-icons-candlestick.svg +5 -37
  204. package/src/svg/mega-menu-icons-ohlc.svg +4 -39
  205. package/src/svg/mega-menu-icons-sunburst.svg +4 -38
  206. package/src/svg/mega-menu-icons-treemap.svg +3 -43
  207. package/src/svg/mega-menu-icons-y-scatter.svg +12 -72
  208. package/src/svg/new-panel-icon.svg +4 -0
  209. package/src/svg/number-type.svg +3 -3
  210. package/src/svg/radio-hover.svg +3 -3
  211. package/src/svg/radio-off.svg +3 -3
  212. package/src/svg/radio-on.svg +3 -3
  213. package/src/svg/status_ok.svg +3 -26
  214. package/src/svg/string-type.svg +4 -8
  215. package/src/svg/theme-icon.svg +5 -5
  216. package/src/themes/blueprint.css +133 -0
  217. package/src/themes/botanical.css +3 -52
  218. package/src/themes/defaults.css +3 -144
  219. package/src/themes/dracula.css +1 -71
  220. package/src/themes/eggplant.css +131 -0
  221. package/src/themes/gruvbox-dark.css +2 -79
  222. package/src/themes/gruvbox.css +2 -40
  223. package/src/themes/icons.css +2 -10
  224. package/src/themes/intl/ar.css +185 -0
  225. package/src/themes/intl/de.css +65 -33
  226. package/src/themes/intl/es.css +60 -28
  227. package/src/themes/intl/fr.css +60 -28
  228. package/src/themes/intl/ja.css +62 -30
  229. package/src/themes/intl/pt.css +58 -26
  230. package/src/themes/intl/zh.css +68 -36
  231. package/src/themes/intl.css +42 -7
  232. package/src/themes/ledger.css +137 -0
  233. package/src/themes/monokai.css +1 -64
  234. package/src/themes/nord.css +131 -0
  235. package/src/themes/phosphor.css +1 -61
  236. package/src/themes/pro-dark.css +1 -52
  237. package/src/themes/pro.css +98 -0
  238. package/src/themes/solarized-dark.css +1 -46
  239. package/src/themes/solarized.css +1 -24
  240. package/src/themes/themes.css +10 -5
  241. package/src/themes/vaporwave.css +1 -57
  242. package/src/themes/velvet.css +132 -0
  243. package/src/ts/column-format.ts +52 -2
  244. package/src/ts/extensions.ts +0 -13
  245. package/src/ts/ts-rs/ViewOnUpdateResp.ts +1 -1
  246. package/src/css/plugin-settings-panel.css +0 -99
  247. package/src/rust/components/modal.rs +0 -177
  248. package/src/rust/custom_elements/copy_dropdown.rs +0 -219
  249. package/src/rust/custom_elements/export_dropdown.rs +0 -216
  250. package/src/rust/custom_elements/modal.rs +0 -299
  251. package/src/svg/dropdown-selector-light.svg +0 -3
  252. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline0.js +0 -0
  253. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline1.js +0 -0
  254. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline2.js +0 -0
  255. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline3.js +0 -0
  256. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline4.js +0 -0
  257. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline5.js +0 -0
  258. /package/src/css/{containers → ui/containers}/scroll-panel.css +0 -0
  259. /package/src/css/{containers → ui/containers}/split-panel.css +0 -0
  260. /package/src/css/{dom → ui/dom}/checkbox.css +0 -0
  261. /package/src/css/{dom → ui/dom}/scrollbar.css +0 -0
  262. /package/src/css/{form → ui/form}/mirrored-textarea.css +0 -0
  263. /package/src/rust/components/{form → code_editor}/highlight.rs +0 -0
  264. /package/src/rust/{components → ui}/containers/sidebar_close_button.rs +0 -0
  265. /package/src/rust/{components → ui}/form/number_input.rs +0 -0
@@ -280,28 +280,14 @@ pub(super) fn create_active_subscriptions(
280
280
 
281
281
  let sub1 = session.table_loaded.add_notify_listener(&cb);
282
282
  let sub2 = session.table_unloaded.add_notify_listener(&cb);
283
- // Snapshot refresh on every RECONCILED commit (SKIP/REUSE/paused
284
- // included), not just `View` construction.
285
283
  let sub3 = session.commit_reconciled.add_notify_listener(&cb);
286
284
  let sub4 = session.view_config_changed.add_notify_listener(&cb);
287
285
  let sub5 = session.title_changed.add_notify_listener(&cb);
288
-
289
- // The `StatusIndicator` spinner count. LEVEL-triggered: the payload
290
- // is the ABSOLUTE in-flight config-run count (RAII-settled by
291
- // `ConfigRunToken`), which the handler ASSIGNS — replacing the
292
- // edge-counted `view_config_changed`+1/`view_created`−1 pair, whose
293
- // pairing the pipeline does not guarantee (stuck-spinner
294
- // regression — see `UPDATE_COUNT_REGRESSION_PLAN.md`).
295
286
  let sub6 = session
296
287
  .run_state_changed
297
288
  .add_listener(ctx.link().callback(UpdateInFlight));
298
289
 
299
- // Stats fetch resolution (populates session.column_stats) triggers
300
- // a fresh `SessionProps` so `column_stats` reaches downstream
301
- // components and the StyleTab re-queries the schema with the
302
- // new value.
303
290
  let sub7 = session.column_stats_changed.add_notify_listener(&cb);
304
-
305
291
  vec![sub1, sub2, sub3, sub4, sub5, sub6, sub7]
306
292
  };
307
293
 
@@ -311,12 +297,6 @@ pub(super) fn create_active_subscriptions(
311
297
  move |_: JsPerspectiveViewerPlugin| UpdateRenderer(Box::new(renderer.to_props(None)))
312
298
  });
313
299
 
314
- // Re-snapshot RendererProps when the plugin_config bucket
315
- // changes (in-tab edit via `send_plugin_config`, JSON paste via
316
- // `restore_and_render`, full clear via `reset_all` with
317
- // `all=true`). Without this, `RendererProps.plugin_config`
318
- // would stay frozen at its construct-time value and `PluginTab`
319
- // would render stale.
320
300
  let cb_plugin_config = ctx.link().callback({
321
301
  let renderer = renderer.clone();
322
302
  move |_: serde_json::Map<String, serde_json::Value>| {
@@ -413,7 +393,7 @@ pub(super) fn inject_active_callbacks(
413
393
  let s = session.clone();
414
394
  let cb = ctx
415
395
  .link()
416
- .callback(move |_: ()| UpdateSessionStats(s.get_table_stats(), s.has_table()));
396
+ .callback(move |_: ()| UpdateSessionStats(s.has_table_cells(), s.has_table()));
417
397
 
418
398
  *session.on_stats_changed.borrow_mut() = Some(cb);
419
399
  }
@@ -27,6 +27,7 @@ mod settings;
27
27
  mod snapshots;
28
28
  mod wiring;
29
29
 
30
+ use std::collections::HashMap;
30
31
  use std::rc::Rc;
31
32
 
32
33
  use futures::channel::oneshot::Sender;
@@ -40,13 +41,13 @@ pub use self::msg::PerspectiveViewerMsg;
40
41
  use self::msg::PerspectiveViewerMsg::*;
41
42
  use self::settings::SettingsGeometry;
42
43
  use self::wiring::*;
43
- use super::font_loader::{FontLoaderProps, FontLoaderStatus};
44
44
  use crate::presentation::{Presentation, PresentationProps};
45
45
  use crate::renderer::{RendererProps, *};
46
46
  use crate::session::{SessionProps, *};
47
47
  use crate::tasks::*;
48
+ use crate::ui::{FontLoaderProps, FontLoaderStatus};
48
49
  use crate::utils::*;
49
- use crate::workspace::Workspace;
50
+ use crate::workspace::{PanelId, Workspace};
50
51
 
51
52
  #[derive(Clone, Properties)]
52
53
  pub struct PerspectiveViewerProps {
@@ -99,6 +100,10 @@ pub struct PerspectiveViewer {
99
100
  on_settings_panel_dimensions_reset: Rc<PubSub<()>>,
100
101
  settings_open: bool,
101
102
 
103
+ /// `removePanel()` completions awaiting their panel's `PanelClosed`
104
+ /// (the deferred eject — see `ClosePanel`).
105
+ pending_closes: HashMap<PanelId, Completion>,
106
+
102
107
  /// Render snapshot of the `Workspace`-owned global filter set (see
103
108
  /// `Workspace::global_filters`); refreshed by `UpdateGlobalFilters` via
104
109
  /// the `filters_changed` subscription. The status-bar chips render from
@@ -210,6 +215,7 @@ impl Component for PerspectiveViewer {
210
215
  on_resize: Default::default(),
211
216
  on_settings_panel_dimensions_reset: Default::default(),
212
217
  settings_open: false,
218
+ pending_closes: HashMap::new(),
213
219
  global_filters: Vec::new(),
214
220
  settings_geometry: Default::default(),
215
221
  session_props,
@@ -305,6 +311,7 @@ impl Component for PerspectiveViewer {
305
311
  LayoutChanged => self.on_layout_changed(ctx),
306
312
  SetActivePanel(id, completion) => self.on_set_active_panel(ctx, id, completion),
307
313
  ClosePanel(id, completion) => self.on_close_panel(ctx, id, completion),
314
+ PanelClosed(id) => self.on_panel_closed(ctx, id),
308
315
  CommitWorkspaceRestore(id) => self.on_commit_workspace_restore(ctx, id),
309
316
  DuplicatePanel(id) => self.on_duplicate_panel(ctx, id),
310
317
  NewPanel(id) => self.on_new_panel(ctx, id),
@@ -325,20 +332,18 @@ impl Component for PerspectiveViewer {
325
332
  ToggleSettingsComplete(update, resolve) => {
326
333
  self.on_toggle_settings_complete(ctx, update, resolve)
327
334
  },
328
- SettingsPanelSizeUpdate(x) => self.on_settings_panel_size_update(x),
329
- SettingsDividerMove(w) => self.on_settings_divider_move(ctx, w),
330
- SettingsDividerPump => self.on_settings_divider_pump(ctx),
331
- SettingsDividerCommit(w) => self.on_settings_divider_commit(ctx, w),
332
- SettingsDividerFinish => self.on_settings_divider_finish(ctx),
335
+ DividerMove(divider, target) => self.on_divider_move(ctx, divider, target),
336
+ DividerPump(divider) => self.on_divider_pump(ctx, divider),
337
+ DividerCommit(divider, target) => self.on_divider_commit(ctx, divider, target),
338
+ DividerFinish(divider) => self.on_divider_finish(ctx, divider),
333
339
  SettingsPanelTabChanged(tab) => self.on_settings_panel_tab_changed(tab),
334
- SettingsPanelAutoWidth(w) => self.on_settings_panel_auto_width(w),
340
+ SettingsPanelAutoWidth(w) => self.on_settings_panel_auto_width(ctx, w),
335
341
  OpenColumnSettings {
336
342
  target,
337
343
  sender,
338
344
  toggle,
339
345
  } => self.on_open_column_settings(ctx, target, sender, toggle),
340
- ColumnSettingsPanelSizeUpdate(x) => self.on_column_settings_panel_size_update(x),
341
- ColumnSettingsPanelAutoWidth(w) => self.on_column_settings_panel_auto_width(w),
346
+ ColumnSettingsPanelAutoWidth(w) => self.on_column_settings_panel_auto_width(ctx, w),
342
347
  ToggleColumnSettingsPin => self.on_toggle_column_settings_pin(ctx),
343
348
  ToggleColumnSettingsPinComplete(resolve) => {
344
349
  self.on_toggle_column_settings_pin_complete(resolve)
@@ -20,12 +20,10 @@ use yew::prelude::*;
20
20
 
21
21
  use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
22
22
  use crate::components::column_selector::{ColumnSelectorColumnRow, InPlaceColumn, PivotColumn};
23
- use crate::components::containers::dragdrop_list::{
24
- DragContext, DragDropList, DragDropListItemProps,
25
- };
26
- use crate::components::containers::select::{Select, SelectItem};
23
+ use crate::components::dragdrop_list::{DragContext, DragDropList, DragDropListItemProps};
27
24
  use crate::presentation::Presentation;
28
25
  use crate::session::{Session, SessionMetadataRc};
26
+ use crate::ui::{IntlLabel, Select, SelectItem};
29
27
  use crate::utils::{AddListener, DragEffect, DragTarget, Subscription};
30
28
 
31
29
  /// The declared capabilities of one window aggregate, for a `source` column
@@ -479,8 +477,9 @@ impl Component for WindowSlotColumn {
479
477
  let presentation = ctx.props().presentation.clone();
480
478
  let action = ctx.props().action;
481
479
  move |event: DragEvent| {
482
- presentation.set_drag_image(&event).unwrap();
483
- presentation.notify_drag_start(column.clone(), DragEffect::Move(action))
480
+ if presentation.set_drag_image(&event) {
481
+ presentation.notify_drag_start(column.clone(), DragEffect::Move(action))
482
+ }
484
483
  }
485
484
  });
486
485
 
@@ -909,7 +908,7 @@ impl Component for WindowEditor {
909
908
  <div id="window-editor-slots">{ source_list }{ order_list }{ partition_list }</div>
910
909
  <div id="window-editor-container">
911
910
  if show_frame {
912
- <div class="column-style-label"><label id="window-frame-label" /></div>
911
+ <div class="column-style-label"><IntlLabel name="window-frame" /></div>
913
912
  <div class="row">
914
913
  <Select<String>
915
914
  id="window-frame-type"
@@ -943,7 +942,7 @@ impl Component for WindowEditor {
943
942
  }
944
943
  if show_offset {
945
944
  <div class="column-style-label">
946
- <label id="window-offset-label" class="indent" />
945
+ <IntlLabel name="window-offset" class="indent" />
947
946
  </div>
948
947
  <div class="row">
949
948
  <input
@@ -957,7 +956,7 @@ impl Component for WindowEditor {
957
956
  }
958
957
  if show_alpha {
959
958
  <div class="column-style-label">
960
- <label id="window-alpha-label" class="indent" />
959
+ <IntlLabel name="window-alpha" class="indent" />
961
960
  </div>
962
961
  <div class="row">
963
962
  <input
@@ -15,7 +15,10 @@ use std::collections::HashSet;
15
15
  use serde::{Deserialize, Serialize};
16
16
  use serde_json::Value;
17
17
 
18
- use super::{CssKind, KeyValueOpts, NumberSeriesStyleDefaultConfig};
18
+ use super::{
19
+ CustomNumberFormatConfig, DatetimeFormatType, KeyValueOpts, NumberSeriesStyleDefaultConfig,
20
+ };
21
+ use crate::utils::{CssKind, GradientStopSpec, canonicalize_gradient_stops};
19
22
 
20
23
  /// The full schema for one column at one point in time. Plugins may return
21
24
  /// different schemas for the same column based on the column's current
@@ -41,6 +44,21 @@ impl ColumnConfigSchema {
41
44
  }
42
45
  out
43
46
  }
47
+
48
+ pub fn leaf_fields(&self) -> Vec<&ControlSpec> {
49
+ fn collect<'a>(fields: &'a [ControlSpec], out: &mut Vec<&'a ControlSpec>) {
50
+ for spec in fields {
51
+ match spec {
52
+ ControlSpec::Group { fields, .. } => collect(fields, out),
53
+ leaf => out.push(leaf),
54
+ }
55
+ }
56
+ }
57
+
58
+ let mut out = vec![];
59
+ collect(&self.fields, &mut out);
60
+ out
61
+ }
44
62
  }
45
63
 
46
64
  /// Discriminated union of widget kinds the viewer can render. Composite
@@ -101,7 +119,12 @@ pub enum ControlSpec {
101
119
  #[serde(default, skip_serializing_if = "std::ops::Not::not")]
102
120
  discrete: bool,
103
121
  },
104
- DatetimeFormat,
122
+ DatetimeFormat {
123
+ /// Plugin-declared default `date_format`, shown by the editor in
124
+ /// unedited fields and elided from serialized configs.
125
+ #[serde(default, skip_serializing_if = "Option::is_none")]
126
+ default: Option<DatetimeFormatType>,
127
+ },
105
128
  StringFormat,
106
129
  NumberSeriesStyle {
107
130
  default: NumberSeriesStyleDefaultConfig,
@@ -109,8 +132,19 @@ pub enum ControlSpec {
109
132
  Symbols {
110
133
  default: KeyValueOpts,
111
134
  },
112
- NumberFormat,
135
+ NumberFormat {
136
+ /// Plugin-declared default format, keyed like `number_format`
137
+ /// itself.
138
+ #[serde(default, skip_serializing_if = "Option::is_none")]
139
+ default: Option<CustomNumberFormatConfig>,
140
+ },
113
141
  AggregateDepth,
142
+
143
+ Group {
144
+ key: String,
145
+ #[serde(default)]
146
+ fields: Vec<ControlSpec>,
147
+ },
114
148
  }
115
149
 
116
150
  #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
@@ -120,31 +154,6 @@ pub struct EnumVariant {
120
154
  pub label: Option<String>,
121
155
  }
122
156
 
123
- /// One stop of a [`ControlSpec::GradientStops`] value in its in-memory
124
- /// form: a `#rrggbb` color at `offset` ∈ `[0, 1]`.
125
- #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
126
- pub struct GradientStopSpec {
127
- pub color: String,
128
- pub offset: f64,
129
- }
130
-
131
- /// The [`ControlSpec::GradientStops`] canonical stop order: offsets
132
- /// clamped to `[0, 1]` and rounded to 3 decimals, stops sorted stably
133
- /// by offset.
134
- pub fn canonicalize_gradient_stops(mut stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
135
- for stop in &mut stops {
136
- stop.offset = (stop.offset.clamp(0.0, 1.0) * 1000.0).round() / 1000.0;
137
- }
138
-
139
- stops.sort_by(|a, b| {
140
- a.offset
141
- .partial_cmp(&b.offset)
142
- .unwrap_or(std::cmp::Ordering::Equal)
143
- });
144
-
145
- stops
146
- }
147
-
148
157
  /// Fit `stops` to a `discrete` field's fixed pair: an over-length value
149
158
  /// keeps only its two end colors, pinned to `0`/`1`.
150
159
  pub fn discrete_pair(stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
@@ -165,37 +174,90 @@ pub fn discrete_pair(stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
165
174
  }
166
175
 
167
176
  impl ColumnConfigSchema {
177
+ pub fn canonicalize(self) -> Self {
178
+ self.canonicalize_defaults().group_format_controls()
179
+ }
180
+
181
+ pub fn group_format_controls(mut self) -> Self {
182
+ fn is_format(spec: &ControlSpec) -> bool {
183
+ matches!(
184
+ spec,
185
+ ControlSpec::NumberFormat { .. }
186
+ | ControlSpec::DatetimeFormat { .. }
187
+ | ControlSpec::StringFormat
188
+ )
189
+ }
190
+
191
+ fn walk(fields: &mut Vec<ControlSpec>) {
192
+ for spec in fields.iter_mut() {
193
+ if let ControlSpec::Group { key, fields } = spec
194
+ && key != "format"
195
+ {
196
+ walk(fields);
197
+ }
198
+ }
199
+
200
+ let first = fields.iter().position(is_format);
201
+ if let Some(first) = first {
202
+ let mut formats = vec![];
203
+ let mut i = first;
204
+ while i < fields.len() {
205
+ if is_format(&fields[i]) {
206
+ formats.push(fields.remove(i));
207
+ } else {
208
+ i += 1;
209
+ }
210
+ }
211
+
212
+ fields.insert(first, ControlSpec::Group {
213
+ key: "format".to_owned(),
214
+ fields: formats,
215
+ });
216
+ }
217
+ }
218
+
219
+ walk(&mut self.fields);
220
+ self
221
+ }
222
+
168
223
  /// Canonicalize every CSS-valued default at schema ingest, dropping
169
224
  /// (and logging) fields whose default fails its kind's reader.
170
225
  pub fn canonicalize_defaults(mut self) -> Self {
171
- self.fields.retain_mut(|spec| {
172
- let (kind, key, default) = match spec {
173
- ControlSpec::Color { key, default } => (CssKind::Color, key, default),
174
- ControlSpec::Palette { key, default, .. } => (CssKind::Palette, key, default),
175
- ControlSpec::GradientStops { key, default, .. } => {
176
- (CssKind::Gradient, key, default)
177
- },
178
- _ => return true,
179
- };
180
-
181
- match kind.canonicalize(default) {
182
- Ok(canonical) => {
183
- *default = canonical;
184
- true
185
- },
186
- Err(error) => {
187
- tracing::error!("Dropping `{key}` — invalid schema default: {error}");
188
- false
189
- },
190
- }
191
- });
226
+ fn canonicalize_specs(fields: &mut Vec<ControlSpec>) {
227
+ fields.retain_mut(|spec| {
228
+ let (kind, key, default) = match spec {
229
+ ControlSpec::Group { fields, .. } => {
230
+ canonicalize_specs(fields);
231
+ return !fields.is_empty();
232
+ },
233
+ ControlSpec::Color { key, default } => (CssKind::Color, key, default),
234
+ ControlSpec::Palette { key, default, .. } => (CssKind::Palette, key, default),
235
+ ControlSpec::GradientStops { key, default, .. } => {
236
+ (CssKind::Gradient, key, default)
237
+ },
238
+ _ => return true,
239
+ };
240
+
241
+ match kind.canonicalize(default) {
242
+ Ok(canonical) => {
243
+ *default = canonical;
244
+ true
245
+ },
246
+ Err(error) => {
247
+ tracing::error!("Dropping `{key}` — invalid schema default: {error}");
248
+ false
249
+ },
250
+ }
251
+ });
252
+ }
192
253
 
254
+ canonicalize_specs(&mut self.fields);
193
255
  self
194
256
  }
195
257
 
196
258
  /// The CSS kind of the control owning `key`, if it is CSS-valued.
197
259
  pub fn css_kind_of(&self, key: &str) -> Option<CssKind> {
198
- self.fields.iter().find_map(|spec| match spec {
260
+ self.leaf_fields().into_iter().find_map(|spec| match spec {
199
261
  ControlSpec::Color { key: k, .. } if k == key => Some(CssKind::Color),
200
262
  ControlSpec::Palette { key: k, .. } if k == key => Some(CssKind::Palette),
201
263
  ControlSpec::GradientStops { key: k, .. } if k == key => Some(CssKind::Gradient),
@@ -212,11 +274,11 @@ impl ControlSpec {
212
274
  /// `columns_config` blob passed to `plugin.restore()`.
213
275
  pub fn serialized_keys(&self) -> Vec<&str> {
214
276
  match self {
215
- ControlSpec::DatetimeFormat => vec!["date_format"],
277
+ ControlSpec::DatetimeFormat { .. } => vec!["date_format"],
216
278
  ControlSpec::StringFormat => vec!["format"],
217
279
  ControlSpec::NumberSeriesStyle { .. } => vec!["chart_type", "stack"],
218
280
  ControlSpec::Symbols { .. } => vec!["symbols"],
219
- ControlSpec::NumberFormat => vec!["number_format"],
281
+ ControlSpec::NumberFormat { .. } => vec!["number_format"],
220
282
  ControlSpec::AggregateDepth => vec!["aggregate_depth"],
221
283
  ControlSpec::Enum { key, .. }
222
284
  | ControlSpec::Bool { key, .. }
@@ -225,6 +287,9 @@ impl ControlSpec {
225
287
  | ControlSpec::Color { key, .. }
226
288
  | ControlSpec::Palette { key, .. }
227
289
  | ControlSpec::GradientStops { key, .. } => vec![key.as_str()],
290
+ ControlSpec::Group { fields, .. } => {
291
+ fields.iter().flat_map(|f| f.serialized_keys()).collect()
292
+ },
228
293
  }
229
294
  }
230
295
  }
@@ -252,3 +317,244 @@ pub fn filter_to_schema(
252
317
  .map(|(k, v)| (k.clone(), v.clone()))
253
318
  .collect()
254
319
  }
320
+
321
+ #[cfg(test)]
322
+ mod tests {
323
+ use serde_json::json;
324
+
325
+ use super::*;
326
+
327
+ fn color(key: &str, default: &str) -> ControlSpec {
328
+ ControlSpec::Color {
329
+ key: key.to_owned(),
330
+ default: default.to_owned(),
331
+ }
332
+ }
333
+
334
+ fn flag(key: &str) -> ControlSpec {
335
+ ControlSpec::Bool {
336
+ key: key.to_owned(),
337
+ default: false,
338
+ }
339
+ }
340
+
341
+ fn group(key: &str, fields: Vec<ControlSpec>) -> ControlSpec {
342
+ ControlSpec::Group {
343
+ key: key.to_owned(),
344
+ fields,
345
+ }
346
+ }
347
+
348
+ #[test]
349
+ fn group_deserializes_recursively() {
350
+ let schema: ColumnConfigSchema = serde_json::from_value(json!({
351
+ "fields": [{
352
+ "kind": "Group",
353
+ "key": "legend",
354
+ "fields": [
355
+ { "kind": "Bool", "key": "legend_on", "default": false },
356
+ {
357
+ "kind": "Group",
358
+ "key": "inner",
359
+ "fields": [{ "kind": "Color", "key": "color", "default": "#ff0000" }]
360
+ }
361
+ ]
362
+ }]
363
+ }))
364
+ .unwrap();
365
+
366
+ let keys = schema.active_keys();
367
+ assert_eq!(
368
+ keys,
369
+ HashSet::from(["legend_on".to_owned(), "color".to_owned()])
370
+ );
371
+
372
+ let leaves = schema.leaf_fields();
373
+ assert_eq!(leaves.len(), 2);
374
+ assert!(
375
+ leaves
376
+ .iter()
377
+ .all(|s| !matches!(s, ControlSpec::Group { .. }))
378
+ );
379
+ }
380
+
381
+ #[test]
382
+ fn grouped_schema_is_equivalent_to_flat() {
383
+ let flat = ColumnConfigSchema {
384
+ fields: vec![flag("stack"), color("color", "#0366d6")],
385
+ };
386
+
387
+ let grouped = ColumnConfigSchema {
388
+ fields: vec![group("series", vec![
389
+ flag("stack"),
390
+ color("color", "#0366d6"),
391
+ ])],
392
+ };
393
+
394
+ assert_eq!(flat.active_keys(), grouped.active_keys());
395
+ assert_eq!(flat.css_kind_of("color"), grouped.css_kind_of("color"));
396
+ assert_eq!(flat.css_kind_of("stack"), grouped.css_kind_of("stack"));
397
+ }
398
+
399
+ #[test]
400
+ fn format_controls_group_and_merge() {
401
+ let schema = ColumnConfigSchema {
402
+ fields: vec![
403
+ ControlSpec::NumberFormat { default: None },
404
+ flag("flag"),
405
+ ControlSpec::StringFormat,
406
+ ],
407
+ }
408
+ .group_format_controls();
409
+
410
+ assert_eq!(schema.fields.len(), 2);
411
+ let ControlSpec::Group { key, fields } = &schema.fields[0] else {
412
+ panic!("expected format group first");
413
+ };
414
+
415
+ assert_eq!(key, "format");
416
+ assert!(matches!(fields[0], ControlSpec::NumberFormat { .. }));
417
+ assert!(matches!(fields[1], ControlSpec::StringFormat));
418
+ assert!(matches!(&schema.fields[1], ControlSpec::Bool { .. }));
419
+
420
+ assert_eq!(
421
+ schema.active_keys(),
422
+ HashSet::from([
423
+ "number_format".to_owned(),
424
+ "format".to_owned(),
425
+ "flag".to_owned()
426
+ ])
427
+ );
428
+ }
429
+
430
+ #[test]
431
+ fn format_grouping_recurses_but_never_double_wraps() {
432
+ let schema = ColumnConfigSchema {
433
+ fields: vec![
434
+ group("format", vec![ControlSpec::NumberFormat { default: None }]),
435
+ group("styling", vec![flag("x"), ControlSpec::DatetimeFormat {
436
+ default: None,
437
+ }]),
438
+ ],
439
+ }
440
+ .group_format_controls();
441
+
442
+ let ControlSpec::Group { key, fields } = &schema.fields[0] else {
443
+ panic!("expected group");
444
+ };
445
+
446
+ assert_eq!(key, "format");
447
+ assert!(matches!(fields[0], ControlSpec::NumberFormat { .. }));
448
+
449
+ let ControlSpec::Group { fields, .. } = &schema.fields[1] else {
450
+ panic!("expected group");
451
+ };
452
+
453
+ assert!(matches!(
454
+ &fields[1],
455
+ ControlSpec::Group { key, fields }
456
+ if key == "format" && matches!(fields[0], ControlSpec::DatetimeFormat { .. })
457
+ ));
458
+ }
459
+
460
+ #[test]
461
+ fn format_controls_deserialize_without_default_payload() {
462
+ let schema: ColumnConfigSchema = serde_json::from_value(json!({
463
+ "fields": [{ "kind": "NumberFormat" }, { "kind": "DatetimeFormat" }]
464
+ }))
465
+ .unwrap();
466
+
467
+ assert!(matches!(&schema.fields[0], ControlSpec::NumberFormat {
468
+ default: None
469
+ }));
470
+ assert!(matches!(&schema.fields[1], ControlSpec::DatetimeFormat {
471
+ default: None
472
+ }));
473
+ }
474
+
475
+ #[test]
476
+ fn number_format_default_payload_deserializes_flattened_families() {
477
+ let schema: ColumnConfigSchema = serde_json::from_value(json!({
478
+ "fields": [{
479
+ "kind": "NumberFormat",
480
+ "default": {
481
+ "notation": "compact",
482
+ "compactDisplay": "short",
483
+ "minimumFractionDigits": 0,
484
+ "maximumFractionDigits": 1
485
+ }
486
+ }]
487
+ }))
488
+ .unwrap();
489
+
490
+ let ControlSpec::NumberFormat {
491
+ default: Some(default),
492
+ } = &schema.fields[0]
493
+ else {
494
+ panic!("expected NumberFormat with default");
495
+ };
496
+
497
+ assert_eq!(
498
+ default._notation,
499
+ Some(crate::config::Notation::Compact(
500
+ crate::config::CompactDisplay::Short
501
+ ))
502
+ );
503
+ assert_eq!(default._style, None);
504
+ assert_eq!(default.minimum_fraction_digits, Some(0.));
505
+ assert_eq!(default.maximum_fraction_digits, Some(1.));
506
+ assert_eq!(
507
+ schema.active_keys(),
508
+ HashSet::from(["number_format".to_owned()])
509
+ );
510
+ }
511
+
512
+ #[test]
513
+ fn datetime_format_default_payload_deserializes_simple_arm() {
514
+ let schema: ColumnConfigSchema = serde_json::from_value(json!({
515
+ "fields": [{
516
+ "kind": "DatetimeFormat",
517
+ "default": { "dateStyle": "medium", "timeStyle": "disabled" }
518
+ }]
519
+ }))
520
+ .unwrap();
521
+
522
+ let ControlSpec::DatetimeFormat {
523
+ default: Some(DatetimeFormatType::Simple(simple)),
524
+ } = &schema.fields[0]
525
+ else {
526
+ panic!("expected DatetimeFormat with Simple default");
527
+ };
528
+
529
+ assert_eq!(
530
+ simple.date_style,
531
+ crate::config::SimpleDatetimeFormat::Medium
532
+ );
533
+ assert_eq!(
534
+ simple.time_style,
535
+ crate::config::SimpleDatetimeFormat::Disabled
536
+ );
537
+ }
538
+
539
+ #[test]
540
+ fn canonicalize_defaults_recurses_and_drops_empty_groups() {
541
+ let schema = ColumnConfigSchema {
542
+ fields: vec![
543
+ group("ok", vec![color("good", "RGB(255,0,0)"), flag("flag")]),
544
+ group("doomed", vec![color("bad", "not-a-color")]),
545
+ ],
546
+ }
547
+ .canonicalize_defaults();
548
+
549
+ assert_eq!(schema.fields.len(), 1);
550
+ let ControlSpec::Group { key, fields } = &schema.fields[0] else {
551
+ panic!("expected group");
552
+ };
553
+
554
+ assert_eq!(key, "ok");
555
+ assert!(matches!(
556
+ &fields[0],
557
+ ControlSpec::Color { default, .. } if default == "#ff0000"
558
+ ));
559
+ }
560
+ }