@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
@@ -61,10 +61,14 @@ pub struct MainPanelProps {
61
61
  /// active panel.
62
62
  pub on_activate_panel: Callback<String>,
63
63
 
64
- /// Fired with a panel id when its frame's close button removes it from the
65
- /// layout, so the root can dispose the panel.
64
+ /// Fired with a panel id from a tab × (an app-initiated close): the root
65
+ /// removes it from the model at once and parks its engines.
66
66
  pub on_close_panel: Callback<String>,
67
67
 
68
+ /// Fired with a panel id once the `regular-layout` tree no longer holds
69
+ /// it, so the root can eject the parked panel.
70
+ pub on_panel_closed: Callback<String>,
71
+
68
72
  /// Fired with `(panel id, command)` when the panel context menu selects a
69
73
  /// command the root executes (New/Duplicate/Reset/ToggleMaster/Close).
70
74
  /// Maximize/Restore are handled HERE (this component owns the layout
@@ -203,6 +207,10 @@ pub struct MainPanel {
203
207
  /// every listener, and nothing ever noticed.
204
208
  listener_target: Option<web_sys::HtmlElement>,
205
209
 
210
+ /// Ids gone from the model whose `removePanel` has been issued to the
211
+ /// layout and not yet committed.
212
+ pending_removals: HashSet<String>,
213
+
206
214
  /// Per-panel `ResizeObserver`s, keyed by panel id, each observing that
207
215
  /// panel's slotted plugin element and resizing only that panel's
208
216
  /// `Renderer`. Bound in `BeforeResize` for the DRAGGED panel only (it is
@@ -347,6 +355,7 @@ impl Component for MainPanel {
347
355
  main_panel_ref: NodeRef::default(),
348
356
  layout_ref: NodeRef::default(),
349
357
  inserted: Vec::new(),
358
+ pending_removals: HashSet::new(),
350
359
  _layout_update_listener: listener,
351
360
  _layout_select_listener: select_listener,
352
361
  _layout_before_resize_listener: before_resize_listener,
@@ -376,6 +385,7 @@ impl Component for MainPanel {
376
385
  }
377
386
 
378
387
  fn changed(&mut self, ctx: &Context<Self>, old: &Self::Properties) -> bool {
388
+ self.close_unstaged_panels(ctx, old);
379
389
  ctx.props() != old
380
390
  }
381
391
 
@@ -403,3 +413,17 @@ impl Component for MainPanel {
403
413
 
404
414
  fn destroy(&mut self, _ctx: &Context<Self>) {}
405
415
  }
416
+
417
+ impl MainPanel {
418
+ /// Report closed every panel gone from the model that the layout never
419
+ /// held, since it has no commit to wait for.
420
+ fn close_unstaged_panels(&self, ctx: &Context<Self>, old: &MainPanelProps) {
421
+ for id in &old.panel_ids {
422
+ if !ctx.props().panel_ids.contains(id)
423
+ && !self.inserted.iter().any(|n| n == id.as_str())
424
+ {
425
+ ctx.props().on_panel_closed.emit(id.as_str().to_owned());
426
+ }
427
+ }
428
+ }
429
+ }
@@ -16,34 +16,32 @@
16
16
 
17
17
  #[cfg(feature = "llm-agent")]
18
18
  pub mod chat_panel;
19
+ pub mod code_editor;
19
20
  pub mod column_dropdown;
20
21
  pub mod column_selector;
21
22
  pub mod column_settings_sidebar;
22
- pub mod containers;
23
- pub mod context_menu;
24
23
  pub mod copy_dropdown;
25
24
  pub mod datetime_column_style;
25
+ pub mod debug_panel;
26
+ pub mod dragdrop_list;
26
27
  pub mod editable_header;
27
28
  pub mod empty_row;
28
29
  pub mod export_dropdown;
29
30
  pub mod expression_editor;
30
31
  pub mod filter_dropdown;
31
- pub mod font_loader;
32
- pub mod form;
33
32
  pub mod function_dropdown;
34
33
  pub mod global_filter_bar;
35
34
  pub mod main_panel;
36
- pub mod modal;
35
+ pub mod new_panel_menu;
37
36
  pub mod number_series_style;
38
37
  pub mod panel_menu;
39
38
  pub mod panel_tab;
40
39
  pub mod plugin_selector;
41
40
  pub mod plugin_tab;
42
- pub mod portal;
43
41
  pub mod render_warning;
42
+ pub mod rows_counter;
44
43
  pub mod settings_panel;
45
44
  pub mod status_bar;
46
- pub mod status_bar_counter;
47
45
  pub mod status_indicator;
48
46
  pub mod string_column_style;
49
47
  pub mod style;
@@ -0,0 +1,97 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ use std::rc::Rc;
14
+
15
+ use yew::prelude::*;
16
+
17
+ /// A pick in the "New" menu: a fresh panel on a hosted `Table`, or a copy of
18
+ /// an existing panel (the same operation as the context menu's "Duplicate").
19
+ #[derive(Clone, PartialEq)]
20
+ pub enum NewPanelPick {
21
+ FromTable { client: String, table: String },
22
+ FromPanel(String),
23
+ }
24
+
25
+ /// `(client name, its hosted table names)` per loaded client — the resolved
26
+ /// output of `queries::fetch_hosted_tables`.
27
+ pub type HostedTables = Rc<Vec<(String, Vec<String>)>>;
28
+
29
+ /// `(panel id, title)` per panel, in layout-insertion order.
30
+ pub type PanelLabels = Rc<Vec<(String, Option<String>)>>;
31
+
32
+ #[derive(Properties, PartialEq)]
33
+ pub struct NewPanelMenuProps {
34
+ /// `None` while the hosted-table fetch is in flight.
35
+ pub tables: Option<HostedTables>,
36
+ pub panels: PanelLabels,
37
+ pub callback: Callback<NewPanelPick>,
38
+ }
39
+
40
+ /// The "New" menu body shared by the status bar dropdown and the context
41
+ /// menu's "New" flyout.
42
+ #[function_component]
43
+ pub fn NewPanelMenu(props: &NewPanelMenuProps) -> Html {
44
+ let pick = |value: NewPanelPick| props.callback.reform(move |_: MouseEvent| value.clone());
45
+
46
+ let tables = match &props.tables {
47
+ None => html! { <span class="no-results">{ "Loading..." }</span> },
48
+ Some(clients) if clients.iter().all(|(_, tables)| tables.is_empty()) => {
49
+ html! { <span class="no-results">{ "No tables" }</span> }
50
+ },
51
+ Some(clients) => {
52
+ let multi = clients.len() > 1;
53
+ clients
54
+ .iter()
55
+ .map(|(client, tables)| {
56
+ let rows = tables.iter().map(|table| {
57
+ let onmousedown = pick(NewPanelPick::FromTable {
58
+ client: client.clone(),
59
+ table: table.clone(),
60
+ });
61
+ html! { <span class="dropdown-menu-item" {onmousedown}>{ table }</span> }
62
+ });
63
+
64
+ html! {
65
+ <>
66
+ if multi { <span class="dropdown-group-sublabel">{ client }</span> }
67
+ { for rows }
68
+ </>
69
+ }
70
+ })
71
+ .collect::<Html>()
72
+ },
73
+ };
74
+
75
+ let panels = if props.panels.is_empty() {
76
+ html! { <span class="no-results">{ "No panels" }</span> }
77
+ } else {
78
+ props
79
+ .panels
80
+ .iter()
81
+ .map(|(id, title)| {
82
+ let onmousedown = pick(NewPanelPick::FromPanel(id.clone()));
83
+ let label = title.clone().unwrap_or_else(|| id.clone());
84
+ html! { <span class="dropdown-menu-item" {onmousedown}>{ label }</span> }
85
+ })
86
+ .collect::<Html>()
87
+ };
88
+
89
+ html! {
90
+ <div class="new-panel-menu">
91
+ <span class="dropdown-group-label" data-label="new-from-table" />
92
+ <div class="dropdown-group-container">{ tables }</div>
93
+ <span class="dropdown-group-label" data-label="new-from-panel" />
94
+ <div class="dropdown-group-container">{ panels }</div>
95
+ </div>
96
+ }
97
+ }
@@ -13,9 +13,8 @@
13
13
  use web_sys::{HtmlInputElement, InputEvent};
14
14
  use yew::prelude::*;
15
15
 
16
- use super::modal::{ModalLink, SetModalLink};
17
- use crate::components::form::select_enum_field::SelectEnumField;
18
16
  use crate::config::*;
17
+ use crate::ui::{IntlLabel, ModalLink, SelectEnumField, SetModalLink};
19
18
  use crate::utils::WeakScope;
20
19
 
21
20
  #[derive(Properties)]
@@ -113,7 +112,7 @@ impl Component for NumberSeriesStyle {
113
112
  });
114
113
  html! {
115
114
  <div class="row">
116
- <label id="stack-label" />
115
+ <IntlLabel name="stack" />
117
116
  <input type="checkbox" id="stack-checkbox" {checked} {oninput} />
118
117
  </div>
119
118
  }
@@ -26,20 +26,23 @@
26
26
  //! `on_close` fires exactly once, when the session ends (blur dismissal, a
27
27
  //! command selection, or the picker closing).
28
28
 
29
+ use std::rc::Rc;
30
+
29
31
  use perspective_js::utils::*;
30
32
  use wasm_bindgen::JsCast;
31
33
  use web_sys::HtmlElement;
32
34
  use yew::prelude::*;
33
35
 
34
- use crate::components::context_menu::{ContextMenu, ContextMenuEntry, ContextMenuItem};
35
36
  use crate::components::copy_dropdown::CopyDropDownMenu;
36
37
  use crate::components::export_dropdown::ExportDropDownMenu;
37
- use crate::components::portal::PortalModal;
38
+ use crate::components::new_panel_menu::{HostedTables, NewPanelMenu, NewPanelPick};
38
39
  use crate::components::style::StyleSurface;
39
40
  use crate::config::*;
40
41
  use crate::js::copy_to_clipboard;
41
42
  use crate::presentation::Presentation;
43
+ use crate::queries::fetch_hosted_tables;
42
44
  use crate::tasks::export_method_to_blob;
45
+ use crate::ui::{ContextMenu, ContextMenuEntry, ContextMenuItem, MODAL_SLOT, PortalModal};
43
46
  use crate::utils::*;
44
47
  use crate::workspace::{PanelId, Workspace};
45
48
 
@@ -48,7 +51,13 @@ use crate::workspace::{PanelId, Workspace};
48
51
  #[derive(Clone, Debug, PartialEq, Eq)]
49
52
  pub enum PanelCommand {
50
53
  New,
51
- NewFrom { client: String, table: String },
54
+ NewFrom {
55
+ client: String,
56
+ table: String,
57
+ },
58
+
59
+ /// A fresh panel copied from the named panel.
60
+ NewFromPanel(String),
52
61
  Duplicate,
53
62
  Reset,
54
63
  Maximize,
@@ -133,8 +142,8 @@ pub enum PanelMenuMsg {
133
142
  }
134
143
 
135
144
  pub struct PanelMenu {
136
- /// The session-long 0×0 cursor anchor element (appended to `<body>`) both
137
- /// stages' `PortalModal`s position against; removed on destroy.
145
+ /// The session-long 0×0 cursor anchor element (a viewer light-DOM child)
146
+ /// both stages' `PortalModal`s position against; removed on destroy.
138
147
  anchor: HtmlElement,
139
148
 
140
149
  /// The open format picker, if the session is in its picker stage.
@@ -143,7 +152,7 @@ pub struct PanelMenu {
143
152
  /// The "New" sub-menu's data: `(client name, its hosted table names)` per
144
153
  /// loaded client, in registration order. `None` while the fetch spawned at
145
154
  /// menu open is still in flight.
146
- tables: Option<Vec<(String, Vec<String>)>>,
155
+ tables: Option<HostedTables>,
147
156
  }
148
157
 
149
158
  impl Component for PanelMenu {
@@ -151,37 +160,20 @@ impl Component for PanelMenu {
151
160
  type Properties = PanelMenuProps;
152
161
 
153
162
  fn create(ctx: &Context<Self>) -> Self {
154
- let clients = ctx.props().workspace.clients();
163
+ let workspace = ctx.props().workspace.clone();
155
164
  let link = ctx.link().clone();
156
165
  ApiFuture::spawn(async move {
157
- let mut tables = Vec::with_capacity(clients.len());
158
- for client in clients {
159
- // A failing client contributes an empty list rather than
160
- // poisoning the whole menu.
161
- let mut names = match client.get_hosted_table_names().await {
162
- Ok(names) => names,
163
- Err(err) => {
164
- tracing::warn!(
165
- "Failed to list tables for `Client` \"{}\": {err}",
166
- client.get_name()
167
- );
168
-
169
- vec![]
170
- },
171
- };
172
-
173
- // Server-side name order isn't meaningful (or stable) — list
174
- // deterministically.
175
- names.sort_unstable();
176
- tables.push((client.get_name().to_owned(), names));
177
- }
178
-
166
+ let tables = fetch_hosted_tables(&workspace).await;
179
167
  link.send_message(PanelMenuMsg::TablesLoaded(tables));
180
168
  Ok(())
181
169
  });
182
170
 
183
171
  Self {
184
- anchor: session_anchor(ctx.props().x, ctx.props().y),
172
+ anchor: session_anchor(
173
+ ctx.props().presentation.viewer_elem(),
174
+ ctx.props().x,
175
+ ctx.props().y,
176
+ ),
185
177
  picker: None,
186
178
  tables: None,
187
179
  }
@@ -213,7 +205,7 @@ impl Component for PanelMenu {
213
205
  false
214
206
  },
215
207
  PanelMenuMsg::TablesLoaded(tables) => {
216
- self.tables = Some(tables);
208
+ self.tables = Some(Rc::new(tables));
217
209
  self.picker.is_none()
218
210
  },
219
211
  }
@@ -229,7 +221,7 @@ impl Component for PanelMenu {
229
221
  fn destroy(&mut self, _ctx: &Context<Self>) {
230
222
  // The session ended (or the parent unmounted mid-session, e.g. the
231
223
  // target panel closed); don't leak the cursor anchor.
232
- let _ = global::body().remove_child(&self.anchor);
224
+ self.anchor.remove();
233
225
  }
234
226
  }
235
227
 
@@ -254,7 +246,7 @@ impl PanelMenu {
254
246
  None => vec![ContextMenuEntry::Submenu {
255
247
  label: "New".to_owned(),
256
248
  on_select: None,
257
- entries: self.new_submenu_entries(ctx),
249
+ entries: vec![ContextMenuEntry::Custom(self.new_submenu_body(ctx))],
258
250
  }],
259
251
  Some(panel_id) => {
260
252
  let can_close = ctx.props().workspace.len() > 1;
@@ -263,7 +255,7 @@ impl PanelMenu {
263
255
  ContextMenuEntry::Submenu {
264
256
  label: "New".to_owned(),
265
257
  on_select: Some(cmd(PanelCommand::New)),
266
- entries: self.new_submenu_entries(ctx),
258
+ entries: vec![ContextMenuEntry::Custom(self.new_submenu_body(ctx))],
267
259
  },
268
260
  item("Duplicate", cmd(PanelCommand::Duplicate), false),
269
261
  item("Reset", cmd(PanelCommand::Reset), false),
@@ -301,7 +293,7 @@ impl PanelMenu {
301
293
  <PortalModal
302
294
  key="perspective-context-menu"
303
295
  tag_name="perspective-context-menu"
304
- surface={StyleSurface::ContextMenu}
296
+ sheet={StyleSurface::ContextMenu.sheet()}
305
297
  target={Some(self.anchor.clone())}
306
298
  own_focus=true
307
299
  on_close={&on_close}
@@ -315,54 +307,35 @@ impl PanelMenu {
315
307
  }
316
308
  }
317
309
 
318
- /// The "New" hover sub-menu's entries: every hosted `Table` name from
319
- /// every loaded `Client`, flat for a single client, grouped under a
320
- /// header row per client otherwise (client names are globally unique, so
321
- /// the grouping also disambiguates table-name collisions).
322
- fn new_submenu_entries(&self, ctx: &Context<Self>) -> Vec<ContextMenuEntry> {
323
- let placeholder = |label: &str| {
324
- vec![ContextMenuEntry::Item(ContextMenuItem {
325
- label: label.to_owned(),
326
- on_select: Callback::noop(),
327
- disabled: true,
328
- })]
329
- };
330
-
331
- let Some(clients) = &self.tables else {
332
- return placeholder("Loading...");
333
- };
334
-
335
- if clients.iter().all(|(_, tables)| tables.is_empty()) {
336
- return placeholder("No tables");
337
- }
338
-
339
- let multi = clients.len() > 1;
340
- let mut entries = Vec::new();
341
- for (client_name, tables) in clients {
342
- if multi {
343
- entries.push(ContextMenuEntry::Header(client_name.clone()));
344
- }
345
-
346
- for table in tables {
347
- let on_select = {
348
- clone!(client_name, table);
349
- ctx.link().callback(move |_| {
350
- PanelMenuMsg::Command(PanelCommand::NewFrom {
351
- client: client_name.clone(),
352
- table: table.clone(),
353
- })
354
- })
355
- };
356
-
357
- entries.push(ContextMenuEntry::Item(ContextMenuItem {
358
- label: table.clone(),
359
- on_select,
360
- disabled: false,
361
- }));
362
- }
363
- }
310
+ /// The "New" hover sub-menu's body, the shared [`NewPanelMenu`].
311
+ fn new_submenu_body(&self, ctx: &Context<Self>) -> Html {
312
+ let panels = Rc::new(
313
+ ctx.props()
314
+ .workspace
315
+ .panel_ids()
316
+ .into_iter()
317
+ .filter_map(|id| {
318
+ let panel = ctx.props().workspace.panel(&id)?;
319
+ let title = panel.session.get_title().filter(|t| !t.is_empty());
320
+ Some((id.as_str().to_owned(), title))
321
+ })
322
+ .collect::<Vec<_>>(),
323
+ );
324
+
325
+ let callback = ctx.link().batch_callback(|pick: NewPanelPick| {
326
+ let cmd = match pick {
327
+ NewPanelPick::FromTable { client, table } => {
328
+ PanelMenuMsg::Command(PanelCommand::NewFrom { client, table })
329
+ },
330
+ NewPanelPick::FromPanel(id) => {
331
+ PanelMenuMsg::Command(PanelCommand::NewFromPanel(id))
332
+ },
333
+ };
334
+
335
+ vec![cmd, PanelMenuMsg::MenuClosed]
336
+ });
364
337
 
365
- entries
338
+ html! { <NewPanelMenu tables={self.tables.clone()} {panels} {callback} /> }
366
339
  }
367
340
 
368
341
  /// Export/Copy format-picker spawned in place of the context menu, anchored
@@ -462,7 +435,7 @@ impl PanelMenu {
462
435
  <PortalModal
463
436
  key={tag_name}
464
437
  {tag_name}
465
- surface={StyleSurface::DropdownMenu}
438
+ sheet={StyleSurface::DropdownMenu.sheet()}
466
439
  {target}
467
440
  own_focus=true
468
441
  {on_close}
@@ -474,20 +447,21 @@ impl PanelMenu {
474
447
  }
475
448
  }
476
449
 
477
- /// Create a session-long 0×0 anchor element at viewport `(x, y)`, appended to
478
- /// `<body>`, for both stages' `PortalModal`s to position against.
479
- fn session_anchor(x: f64, y: f64) -> HtmlElement {
450
+ /// Create the session-long 0×0 cursor anchor at viewport `(x, y)` as a
451
+ /// light-DOM child of `viewer` in the modal slot.
452
+ fn session_anchor(viewer: &HtmlElement, x: f64, y: f64) -> HtmlElement {
480
453
  let anchor: HtmlElement = global::document()
481
454
  .create_element("div")
482
455
  .unwrap()
483
456
  .unchecked_into();
484
457
 
458
+ let _ = anchor.set_attribute("slot", MODAL_SLOT);
485
459
  let style = anchor.style();
486
460
  let _ = style.set_property("position", "fixed");
487
461
  let _ = style.set_property("left", &format!("{x}px"));
488
462
  let _ = style.set_property("top", &format!("{y}px"));
489
463
  let _ = style.set_property("width", "0px");
490
464
  let _ = style.set_property("height", "0px");
491
- let _ = global::body().append_child(&anchor);
465
+ let _ = viewer.append_child(&anchor);
492
466
  anchor
493
467
  }