@perspective-dev/viewer 5.3.1 → 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 (255) 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.inline.js +9 -9
  32. package/dist/esm/perspective-viewer.inline.js.map +3 -3
  33. package/dist/esm/perspective-viewer.js +5 -5
  34. package/dist/esm/perspective-viewer.js.map +2 -2
  35. package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +1 -1
  36. package/dist/wasm/perspective-viewer.d.ts +62 -73
  37. package/dist/wasm/perspective-viewer.js +226 -238
  38. package/dist/wasm/perspective-viewer.wasm +0 -0
  39. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -28
  40. package/package.json +7 -7
  41. package/src/css/_context_menu.css +1 -1
  42. package/src/css/_dropdown_menu.css +1 -1
  43. package/src/css/_viewer_bundle.css +7 -8
  44. package/src/css/column-dropdown.css +0 -3
  45. package/src/css/column-selector.css +8 -5
  46. package/src/css/column-settings-panel.css +0 -225
  47. package/src/css/column-style.css +13 -0
  48. package/src/css/config-selector.css +18 -10
  49. package/src/css/filter-dropdown.css +5 -5
  50. package/src/css/form/debug.css +28 -4
  51. package/src/css/function-dropdown.css +5 -5
  52. package/src/css/panel-tab.css +23 -0
  53. package/src/css/plugin-selector.css +5 -5
  54. package/src/css/render-warning.css +1 -1
  55. package/src/css/status-bar.css +52 -21
  56. package/src/css/type-icon.css +1 -3
  57. package/src/css/{containers → ui/containers}/context-menu.css +22 -16
  58. package/src/css/ui/containers/dropdown-menu-items.css +66 -0
  59. package/src/css/{containers → ui/containers}/dropdown-menu.css +8 -53
  60. package/src/css/viewer.css +32 -26
  61. package/src/rust/components/chat_panel.rs +1 -1
  62. package/src/rust/components/{form/code_editor.rs → code_editor.rs} +4 -2
  63. package/src/rust/components/column_dropdown.rs +2 -2
  64. package/src/rust/components/column_selector/active_column.rs +15 -9
  65. package/src/rust/components/column_selector/aggregate_selector.rs +1 -1
  66. package/src/rust/components/column_selector/config_selector.rs +2 -2
  67. package/src/rust/components/column_selector/filter_column.rs +8 -5
  68. package/src/rust/components/column_selector/inactive_column.rs +4 -2
  69. package/src/rust/components/column_selector/pivot_column.rs +4 -3
  70. package/src/rust/components/column_selector/sort_column.rs +7 -4
  71. package/src/rust/components/column_selector.rs +3 -3
  72. package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +1 -1
  73. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +15 -17
  74. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +1 -1
  75. package/src/rust/components/column_settings_sidebar/style_tab.rs +4 -5
  76. package/src/rust/components/column_settings_sidebar.rs +3 -4
  77. package/src/rust/components/copy_dropdown.rs +1 -1
  78. package/src/rust/components/datetime_column_style/custom.rs +1 -3
  79. package/src/rust/components/datetime_column_style/simple.rs +1 -2
  80. package/src/rust/components/datetime_column_style.rs +1 -2
  81. package/src/rust/components/{form/debug.rs → debug_panel.rs} +5 -5
  82. package/src/rust/components/{containers/dragdrop_list.rs → dragdrop_list.rs} +2 -10
  83. package/src/rust/components/export_dropdown.rs +1 -1
  84. package/src/rust/components/expression_editor.rs +1 -1
  85. package/src/rust/components/filter_dropdown.rs +2 -2
  86. package/src/rust/components/function_dropdown.rs +2 -2
  87. package/src/rust/components/main_panel/frame_theme.rs +3 -59
  88. package/src/rust/components/main_panel/reconcile.rs +9 -4
  89. package/src/rust/components/main_panel/render.rs +62 -11
  90. package/src/rust/components/main_panel/update.rs +39 -10
  91. package/src/rust/components/main_panel.rs +26 -2
  92. package/src/rust/components/mod.rs +5 -7
  93. package/src/rust/components/new_panel_menu.rs +97 -0
  94. package/src/rust/components/number_series_style.rs +2 -3
  95. package/src/rust/components/panel_menu.rs +61 -87
  96. package/src/rust/components/panel_tab.rs +100 -43
  97. package/src/rust/components/plugin_tab.rs +1 -1
  98. package/src/rust/components/render_warning.rs +2 -2
  99. package/src/rust/components/{status_bar_counter.rs → rows_counter.rs} +10 -10
  100. package/src/rust/components/settings_panel.rs +2 -2
  101. package/src/rust/components/status_bar.rs +113 -19
  102. package/src/rust/components/status_indicator.rs +5 -14
  103. package/src/rust/components/string_column_style.rs +1 -2
  104. package/src/rust/components/{style/mod.rs → style.rs} +32 -15
  105. package/src/rust/components/style_controls/number_string_format/digits_section.rs +3 -5
  106. package/src/rust/components/style_controls/number_string_format/misc_section.rs +1 -1
  107. package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -1
  108. package/src/rust/components/viewer/msg.rs +43 -30
  109. package/src/rust/components/viewer/panels.rs +50 -19
  110. package/src/rust/components/viewer/render.rs +15 -13
  111. package/src/rust/components/viewer/settings.rs +137 -68
  112. package/src/rust/components/viewer/snapshots.rs +5 -10
  113. package/src/rust/components/viewer/wiring.rs +1 -21
  114. package/src/rust/components/viewer.rs +15 -10
  115. package/src/rust/components/window_editor.rs +8 -9
  116. package/src/rust/config/column_config_schema.rs +2 -27
  117. package/src/rust/config/mod.rs +0 -2
  118. package/src/rust/config/workspace_config.rs +2 -1
  119. package/src/rust/custom_elements/debug_plugin.rs +1 -10
  120. package/src/rust/custom_elements/mod.rs +0 -3
  121. package/src/rust/custom_elements/viewer.rs +0 -18
  122. package/src/rust/js/plugin.rs +0 -86
  123. package/src/rust/lib.rs +12 -15
  124. package/src/rust/presentation/column_locator.rs +3 -7
  125. package/src/rust/presentation.rs +68 -38
  126. package/src/rust/queries/hosted_tables.rs +38 -0
  127. package/src/rust/queries/mod.rs +2 -0
  128. package/src/rust/queries/palette_set.rs +1 -1
  129. package/src/rust/renderer/activate.rs +1 -3
  130. package/src/rust/renderer/dispatch.rs +83 -22
  131. package/src/rust/renderer/limits.rs +24 -5
  132. package/src/rust/renderer/plugin_config.rs +1 -0
  133. package/src/rust/renderer/props.rs +2 -1
  134. package/src/rust/renderer.rs +14 -27
  135. package/src/rust/session/props.rs +4 -3
  136. package/src/rust/session/view_subscription.rs +19 -6
  137. package/src/rust/session.rs +38 -70
  138. package/src/rust/tasks/create_panel.rs +1 -1
  139. package/src/rust/tasks/dismiss_render_warning.rs +1 -1
  140. package/src/rust/tasks/eject.rs +1 -0
  141. package/src/rust/tasks/pipeline.rs +0 -60
  142. package/src/rust/tasks/presize_panels.rs +24 -1
  143. package/src/rust/tasks/restore_and_render.rs +0 -17
  144. package/src/rust/tasks/send_column_config.rs +1 -1
  145. package/src/rust/tasks/send_plugin_config.rs +1 -1
  146. package/src/rust/tasks/set_edit_mode.rs +1 -1
  147. package/src/rust/{components → ui}/containers/control_group.rs +25 -3
  148. package/src/rust/{components → ui}/containers/dropdown_menu.rs +5 -2
  149. package/src/rust/{components → ui}/containers/mod.rs +0 -4
  150. package/src/rust/{components → ui}/containers/scroll_panel.rs +0 -22
  151. package/src/rust/{components → ui}/containers/scroll_panel_item.rs +0 -13
  152. package/src/rust/{components → ui}/containers/select.rs +0 -6
  153. package/src/rust/{components → ui}/containers/sidebar.rs +5 -32
  154. package/src/rust/{components → ui}/containers/split_panel.rs +8 -80
  155. package/src/rust/{components → ui}/containers/tab_list.rs +15 -9
  156. package/src/rust/{components → ui}/context_menu.rs +8 -13
  157. package/src/rust/{presentation/drag_helpers.rs → ui/dragdrop.rs} +26 -31
  158. package/src/rust/{components → ui}/font_loader.rs +0 -26
  159. package/src/rust/{components → ui}/form/color_selector.rs +4 -2
  160. package/src/rust/ui/form/intl_label.rs +81 -0
  161. package/src/rust/{components → ui}/form/mirrored_textarea.rs +0 -53
  162. package/src/rust/{components → ui}/form/mod.rs +1 -10
  163. package/src/rust/{components → ui}/form/multi_stop_gradient_selector.rs +4 -5
  164. package/src/rust/{components → ui}/form/named_value_picker.rs +1 -6
  165. package/src/rust/{components → ui}/form/number_field.rs +1 -1
  166. package/src/rust/{components → ui}/form/number_range_field.rs +1 -1
  167. package/src/rust/{components → ui}/form/optional_field.rs +3 -1
  168. package/src/rust/{components → ui}/form/palette_selector.rs +4 -5
  169. package/src/rust/{components → ui}/form/select_enum_field.rs +2 -2
  170. package/src/rust/{components → ui}/form/select_value_field.rs +2 -2
  171. package/src/rust/ui/mod.rs +56 -0
  172. package/src/rust/ui/modal.rs +61 -0
  173. package/src/rust/{components → ui}/portal.rs +174 -79
  174. package/src/rust/{components/style/style_provider.rs → ui/style.rs} +4 -34
  175. package/src/rust/utils/completion.rs +0 -6
  176. package/src/rust/{config/css_scale.rs → utils/css.rs} +38 -35
  177. package/src/rust/utils/mod.rs +2 -0
  178. package/src/rust/workspace.rs +35 -0
  179. package/src/svg/boolean-type.svg +3 -2
  180. package/src/svg/checkbox-checked-icon.svg +4 -7
  181. package/src/svg/checkbox-hover.svg +3 -3
  182. package/src/svg/checkbox-off.svg +2 -3
  183. package/src/svg/checkbox-on.svg +3 -3
  184. package/src/svg/checkbox-unchecked-icon.svg +3 -3
  185. package/src/svg/datagrid-select-column.svg +8 -15
  186. package/src/svg/datagrid-select-region.svg +8 -15
  187. package/src/svg/datagrid-select-row-tree.svg +7 -12
  188. package/src/svg/datagrid-select-row.svg +8 -13
  189. package/src/svg/date-type.svg +4 -23
  190. package/src/svg/drawer-tab-hover.svg +4 -6
  191. package/src/svg/drawer-tab-invert-hover.svg +5 -5
  192. package/src/svg/drawer-tab-invert.svg +3 -5
  193. package/src/svg/drawer-tab.svg +5 -5
  194. package/src/svg/mega-menu-icons-candlestick.svg +5 -37
  195. package/src/svg/mega-menu-icons-ohlc.svg +4 -39
  196. package/src/svg/mega-menu-icons-sunburst.svg +4 -38
  197. package/src/svg/mega-menu-icons-treemap.svg +3 -43
  198. package/src/svg/mega-menu-icons-y-scatter.svg +12 -72
  199. package/src/svg/new-panel-icon.svg +4 -0
  200. package/src/svg/number-type.svg +3 -3
  201. package/src/svg/radio-hover.svg +3 -3
  202. package/src/svg/radio-off.svg +3 -3
  203. package/src/svg/radio-on.svg +3 -3
  204. package/src/svg/status_ok.svg +3 -26
  205. package/src/svg/string-type.svg +4 -8
  206. package/src/svg/theme-icon.svg +5 -5
  207. package/src/themes/blueprint.css +0 -57
  208. package/src/themes/botanical.css +0 -36
  209. package/src/themes/defaults.css +1 -24
  210. package/src/themes/dracula.css +0 -58
  211. package/src/themes/eggplant.css +0 -58
  212. package/src/themes/gruvbox-dark.css +0 -65
  213. package/src/themes/gruvbox.css +1 -39
  214. package/src/themes/icons.css +1 -8
  215. package/src/themes/intl/ar.css +185 -0
  216. package/src/themes/intl/de.css +45 -35
  217. package/src/themes/intl/es.css +40 -30
  218. package/src/themes/intl/fr.css +39 -29
  219. package/src/themes/intl/ja.css +41 -31
  220. package/src/themes/intl/pt.css +37 -27
  221. package/src/themes/intl/zh.css +47 -37
  222. package/src/themes/intl.css +16 -3
  223. package/src/themes/ledger.css +1 -59
  224. package/src/themes/monokai.css +0 -51
  225. package/src/themes/nord.css +0 -55
  226. package/src/themes/phosphor.css +0 -48
  227. package/src/themes/pro-dark.css +0 -37
  228. package/src/themes/pro.css +3 -23
  229. package/src/themes/solarized-dark.css +0 -31
  230. package/src/themes/solarized.css +1 -24
  231. package/src/themes/vaporwave.css +0 -44
  232. package/src/themes/velvet.css +0 -60
  233. package/src/ts/extensions.ts +0 -13
  234. package/src/ts/ts-rs/ViewOnUpdateResp.ts +1 -1
  235. package/src/css/plugin-settings-panel.css +0 -171
  236. package/src/rust/components/modal.rs +0 -177
  237. package/src/rust/custom_elements/copy_dropdown.rs +0 -219
  238. package/src/rust/custom_elements/export_dropdown.rs +0 -216
  239. package/src/rust/custom_elements/modal.rs +0 -299
  240. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline0.js +0 -0
  241. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline1.js +0 -0
  242. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline2.js +0 -0
  243. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline3.js +0 -0
  244. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline4.js +0 -0
  245. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline5.js +0 -0
  246. /package/src/css/{containers → ui/containers}/scroll-panel.css +0 -0
  247. /package/src/css/{containers → ui/containers}/split-panel.css +0 -0
  248. /package/src/css/{containers → ui/containers}/tabs.css +0 -0
  249. /package/src/css/{dom → ui/dom}/checkbox.css +0 -0
  250. /package/src/css/{dom → ui/dom}/scrollbar.css +0 -0
  251. /package/src/css/{dom → ui/dom}/select.css +0 -0
  252. /package/src/css/{form → ui/form}/mirrored-textarea.css +0 -0
  253. /package/src/rust/components/{form → code_editor}/highlight.rs +0 -0
  254. /package/src/rust/{components → ui}/containers/sidebar_close_button.rs +0 -0
  255. /package/src/rust/{components → ui}/form/number_input.rs +0 -0
@@ -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
  }
@@ -19,6 +19,10 @@ use wasm_bindgen::prelude::*;
19
19
  use web_sys::*;
20
20
  use yew::prelude::*;
21
21
 
22
+ use crate::components::rows_counter::RowsCounter;
23
+ use crate::session::{Session, ViewStats};
24
+ use crate::utils::Subscription;
25
+
22
26
  #[wasm_bindgen(inline_js = r#"
23
27
  export function define_panel_tab(name) {
24
28
  if (!customElements.get(name)) {
@@ -41,8 +45,6 @@ thread_local! {
41
45
  static ELEMENT_DEFINED: Cell<bool> = const { Cell::new(false) };
42
46
 
43
47
  /// The shared, constructed stylesheet adopted into every tab's ShadowRoot.
44
- /// Built once from `panel-tab.css`; a constructed `CSSStyleSheet` is cheap
45
- /// and safe to adopt into arbitrarily many roots.
46
48
  static TAB_SHEET: CssStyleSheet = {
47
49
  let sheet = CssStyleSheet::new().unwrap();
48
50
  sheet.replace_sync(include_str!("../../css/panel-tab.css"))
@@ -55,11 +57,6 @@ thread_local! {
55
57
  /// lifecycle callbacks — it exists only to own a ShadowRoot into which each
56
58
  /// tab's contents are rendered (so the tab's structure CSS is encapsulated in
57
59
  /// its `adoptedStyleSheets` rather than injected into `document.head`).
58
- ///
59
- /// The host stays a *light-DOM* child of the viewer, so the per-panel `theme`
60
- /// attr is still matched by the document theme rules (`perspective-viewer
61
- /// [theme="X"]`); the `--psp-*` they set are inherited across the shadow
62
- /// boundary into the tab's contents.
63
60
  fn ensure_custom_element() {
64
61
  ELEMENT_DEFINED.with(|defined| {
65
62
  if !defined.get() {
@@ -90,16 +87,6 @@ fn adopt_sheet(shadow_root: &Element) {
90
87
  /// `<regular-layout-frame>` titlebar via a `<slot name="tab-{id}" slot="tab">`
91
88
  /// (rendered by `MainPanel`); the tab's *contents* live in the host's
92
89
  /// **ShadowRoot**, whose `adoptedStyleSheets` carry the structure CSS.
93
- ///
94
- /// Keeping the host in the light DOM — like the plugin — is what lets
95
- /// *document* theme CSS (`perspective-viewer [theme="X"]`) reach it, so each
96
- /// panel's chrome is themed by the native cascade instead of a runtime-inlined
97
- /// string; the `--psp-*` those rules set are inherited across the shadow
98
- /// boundary into the contents. The host carries `part="tab"`, which
99
- /// `<regular-layout-frame>`'s pointer handler treats as a drag handle
100
- /// (regular-layout@>=0.6.0's custom-tab contract); the title is
101
- /// `pointer-events:none` so clicks fall through to the host (like the built-in
102
- /// `<regular-layout-tab>`).
103
90
  #[derive(Properties, PartialEq)]
104
91
  pub struct PanelTabProps {
105
92
  /// The `<perspective-viewer>` host element; the tab is attached here as a
@@ -112,6 +99,10 @@ pub struct PanelTabProps {
112
99
  /// The panel's title; falls back to the id when `None`.
113
100
  pub title: Option<String>,
114
101
 
102
+ /// This panel's own `Session`, whose `stats_changed` the tab subscribes
103
+ /// to directly so a stats tick re-renders only this tab.
104
+ pub session: Session,
105
+
115
106
  /// This panel's effective theme. Reflected onto the host's `theme`
116
107
  /// attribute so the document theme rules (`perspective-viewer [theme="X"]`)
117
108
  /// theme the tab per-panel via the native cascade.
@@ -147,6 +138,11 @@ pub struct PanelTabProps {
147
138
  /// at zero panels).
148
139
  pub is_settings_open: bool,
149
140
 
141
+ /// `true` while this panel's close awaits its layout commit, which keeps
142
+ /// the tab rendered but inert.
143
+ #[prop_or_default]
144
+ pub closing: bool,
145
+
150
146
  /// `false` for a lone panel — suppresses the tab rearrange-drag.
151
147
  /// `<regular-layout-frame>` arms a drag from any `part="tab"` pointerdown,
152
148
  /// but a lone panel has nowhere to drop, so the host `pointerdown` handler
@@ -189,56 +185,81 @@ const TITLE_PLACEHOLDER: &str = "untitled";
189
185
 
190
186
  pub enum PanelTabMsg {
191
187
  /// A pointerdown on the tab host. Selects the panel; and when it's the
192
- /// second within [`DBLCLICK_MS`], enters title-edit mode. Carries the
193
- /// event `timeStamp` (ms). We synthesize the double-click from pointerdown
194
- /// because `<regular-layout-frame>` calls `setPointerCapture` +
195
- /// `preventDefault` on the tab's pointerdown (to arm a drag), which
196
- /// suppresses the browser's synthesized `click`/`dblclick` — so a native
197
- /// `dblclick` listener never fires on the tab.
188
+ /// second within [`DBLCLICK_MS`], enters title-edit mode.
198
189
  PointerDown(f64),
199
190
  Close,
191
+
200
192
  /// The tab's open-settings button (shown while the settings sidebar is
201
193
  /// closed, in the close button's place).
202
194
  OpenSettings,
203
195
  ContextMenu(f64, f64),
196
+
204
197
  /// Track the live `<input>` value while editing (drives the auto-sizer).
205
198
  EditInput(String),
199
+
206
200
  /// Commit the edited title to the panel's session (blur / Enter).
207
201
  CommitEdit,
202
+
208
203
  /// Abandon the edit, restoring the previous title (Escape).
209
204
  CancelEdit,
205
+
206
+ /// This panel's `Session::stats_changed` fired; re-read the stats.
207
+ StatsChanged,
210
208
  }
211
209
 
212
210
  pub struct PanelTab {
213
211
  host: HtmlElement,
214
- /// The host's open ShadowRoot; the tab's contents are portaled here (stored
215
- /// as `Element` for `create_portal`, matching `PortalModal`).
216
212
  shadow_root: Element,
217
- /// `pointerdown` listener on the host (kept alive); selects this panel and
218
- /// synthesizes double-click-to-edit (see [`PanelTabMsg::PointerDown`]).
219
213
  _pointerdown: Closure<dyn FnMut(PointerEvent)>,
220
- /// `contextmenu` listener on the host (kept alive); opens the panel menu.
221
214
  _contextmenu: Closure<dyn FnMut(MouseEvent)>,
222
- /// Current `draggable` prop, shared into the `pointerdown` closure so a
223
- /// prop change (lone ⇄ multi panel) takes effect without re-creating it.
224
215
  draggable: Rc<Cell<bool>>,
216
+
225
217
  /// `timeStamp` (ms) of the last host pointerdown, for synthesizing
226
- /// double-clicks. `NEG_INFINITY` until the first pointerdown.
218
+ /// double-clicks.
227
219
  last_pointerdown: f64,
220
+
228
221
  /// `true` while the title is being edited (renders an `<input>`).
229
222
  editing: bool,
223
+
230
224
  /// Live edited value; controls the `<input>` and the auto-sizer width.
231
225
  edit_value: String,
226
+
232
227
  /// The edit `<input>`, for focusing on edit entry.
233
228
  input_ref: NodeRef,
229
+
234
230
  /// Focus + select-all the input on the next render after entering edit
235
231
  /// mode.
236
232
  focus_pending: bool,
233
+
234
+ /// The `theme` prop value last written to the host.
235
+ stamped_theme: Option<Option<String>>,
236
+
237
+ /// This panel's `Table`/`View` dimensions, rendered as the `RowsCounter`
238
+ /// after the title.
239
+ stats: Option<ViewStats>,
240
+
241
+ /// Subscription to `session.stats_changed`, armed only while the tab is
242
+ /// `visible`.
243
+ _stats_sub: Option<Subscription>,
237
244
  }
238
245
 
239
246
  impl PanelTab {
240
247
  fn slot_name(panel_id: &str) -> String {
241
- format!("tab-{panel_id}")
248
+ format!("tab-{}", panel_id)
249
+ }
250
+
251
+ /// Arm the `session.stats_changed` subscription and take a fresh stats
252
+ /// snapshot.
253
+ fn arm_stats(&mut self, ctx: &Context<Self>) {
254
+ let cb = ctx.link().callback(|_: ()| PanelTabMsg::StatsChanged);
255
+ self._stats_sub = Some(ctx.props().session.stats_changed.add_notify_listener(&cb));
256
+ self.stats = ctx.props().session.get_table_stats();
257
+ }
258
+
259
+ /// Drop the stats subscription and snapshot.
260
+ fn disarm_stats(&mut self) {
261
+ self._stats_sub = None;
262
+ self.stats = None;
242
263
  }
243
264
 
244
265
  /// Enter title-edit mode, seeding the input with the *real* title (empty
@@ -261,8 +282,8 @@ impl PanelTab {
261
282
  /// tab of every stack, not just the single active panel; `single`/`multi`
262
283
  /// reflect the host viewer's panel count. The shadow CSS keys off
263
284
  /// `:host(.active)` / `:host(.visible)` / `:host(.single)`.
264
- fn sync_class(&self, active: bool, visible: bool, single: bool) {
265
- let mut class = Vec::with_capacity(3);
285
+ fn sync_class(&self, active: bool, visible: bool, single: bool, closing: bool) {
286
+ let mut class = Vec::with_capacity(4);
266
287
  if active {
267
288
  class.push("active");
268
289
  }
@@ -271,6 +292,10 @@ impl PanelTab {
271
292
  class.push("visible");
272
293
  }
273
294
 
295
+ if closing {
296
+ class.push("closing");
297
+ }
298
+
274
299
  class.push(if single { "single" } else { "multi" });
275
300
  let _ = self.host.set_attribute("class", &class.join(" "));
276
301
  }
@@ -331,7 +356,7 @@ impl Component for PanelTab {
331
356
  let _ = host
332
357
  .add_event_listener_with_callback("contextmenu", contextmenu.as_ref().unchecked_ref());
333
358
 
334
- Self {
359
+ let mut tab = Self {
335
360
  host,
336
361
  shadow_root,
337
362
  _pointerdown: pointerdown,
@@ -342,7 +367,16 @@ impl Component for PanelTab {
342
367
  edit_value: String::new(),
343
368
  input_ref: NodeRef::default(),
344
369
  focus_pending: false,
370
+ stamped_theme: None,
371
+ stats: None,
372
+ _stats_sub: None,
373
+ };
374
+
375
+ if ctx.props().visible {
376
+ tab.arm_stats(ctx);
345
377
  }
378
+
379
+ tab
346
380
  }
347
381
 
348
382
  fn changed(&mut self, ctx: &Context<Self>, old: &Self::Properties) -> bool {
@@ -352,6 +386,12 @@ impl Component for PanelTab {
352
386
  .set_attribute("slot", &Self::slot_name(&ctx.props().panel_id));
353
387
  }
354
388
 
389
+ if !ctx.props().visible {
390
+ self.disarm_stats();
391
+ } else if self._stats_sub.is_none() || ctx.props().session != old.session {
392
+ self.arm_stats(ctx);
393
+ }
394
+
355
395
  self.draggable.set(ctx.props().draggable);
356
396
  true
357
397
  }
@@ -400,6 +440,12 @@ impl Component for PanelTab {
400
440
  self.editing = false;
401
441
  true
402
442
  },
443
+ PanelTabMsg::StatsChanged => {
444
+ let stats = ctx.props().session.get_table_stats();
445
+ let changed = stats != self.stats;
446
+ self.stats = stats;
447
+ changed
448
+ },
403
449
  }
404
450
  }
405
451
 
@@ -465,6 +511,7 @@ impl Component for PanelTab {
465
511
  <span class="psp-tab-caret" />
466
512
  // <span class="psp-tab-grip" />
467
513
  { title_html }
514
+ if ctx.props().visible { <RowsCounter stats={self.stats.clone()} /> }
468
515
  if ctx.props().is_master { <span class="psp-tab-master" /> }
469
516
  if !ctx.props().is_settings_open {
470
517
  <button class="psp-tab-settings" onpointerdown={on_open_settings} />
@@ -478,14 +525,24 @@ impl Component for PanelTab {
478
525
  }
479
526
 
480
527
  fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
481
- self.sync_class(ctx.props().active, ctx.props().visible, ctx.props().single);
482
- match &ctx.props().theme {
483
- Some(theme) => {
484
- let _ = self.host.set_attribute("theme", theme);
485
- },
486
- None => {
487
- let _ = self.host.remove_attribute("theme");
488
- },
528
+ self.sync_class(
529
+ ctx.props().active,
530
+ ctx.props().visible,
531
+ ctx.props().single,
532
+ ctx.props().closing,
533
+ );
534
+ let theme = &ctx.props().theme;
535
+ if self.stamped_theme.as_ref() != Some(theme) {
536
+ match theme {
537
+ Some(theme) => {
538
+ let _ = self.host.set_attribute("theme", theme);
539
+ },
540
+ None => {
541
+ let _ = self.host.remove_attribute("theme");
542
+ },
543
+ }
544
+
545
+ self.stamped_theme = Some(theme.clone());
489
546
  }
490
547
 
491
548
  if !self.host.is_connected() {
@@ -22,13 +22,13 @@ use yew::prelude::*;
22
22
  use crate::components::column_settings_sidebar::style_tab::primitive_field::{
23
23
  BoolField, ColorField, EnumField, NumberFieldPrimitive,
24
24
  };
25
- use crate::components::containers::control_group::ControlGroup;
26
25
  use crate::config::ControlSpec;
27
26
  use crate::presentation::Presentation;
28
27
  use crate::queries::get_plugin_config_schema;
29
28
  use crate::renderer::Renderer;
30
29
  use crate::session::Session;
31
30
  use crate::tasks::send_plugin_config;
31
+ use crate::ui::ControlGroup;
32
32
  use crate::utils::PtrEqRc;
33
33
 
34
34
  #[derive(Clone, PartialEq, Properties)]
@@ -27,13 +27,13 @@ pub struct RenderWarningProps {
27
27
  pub fn render_warning(props: &RenderWarningProps) -> Html {
28
28
  let dimensions = props.dimensions;
29
29
  let (col_warn, row_warn) = if let Some(limits) = dimensions {
30
- let col_warn = if limits.max_cols.is_some_and(|x| x < limits.num_cols) {
30
+ let col_warn = if limits.is_col_capped() {
31
31
  Some((limits.max_cols.unwrap(), limits.num_cols))
32
32
  } else {
33
33
  None
34
34
  };
35
35
 
36
- let row_warn = if limits.max_rows.is_some_and(|x| x < limits.num_rows) {
36
+ let row_warn = if limits.is_row_capped() {
37
37
  Some((
38
38
  limits.num_cols * limits.max_rows.unwrap(),
39
39
  limits.num_cols * limits.num_rows,
@@ -15,17 +15,17 @@ use yew::prelude::*;
15
15
  use crate::session::ViewStats;
16
16
  use crate::utils::u32Ext;
17
17
 
18
- /// Props are now pure value types: no `Session` handle, no PubSub
19
- /// subscriptions. The parent passes an updated `stats` whenever the
20
- /// underlying data changes (driven by the root's `UpdateSession` message).
18
+ /// Pure value props: no `Session` handle, no PubSub subscriptions. The parent
19
+ /// (`PanelTab`) passes its panel's `stats` snapshot, refreshed by the root on
20
+ /// that panel's `Session::stats_changed`.
21
21
  #[derive(PartialEq, Properties)]
22
- pub struct StatusBarRowsCounterProps {
22
+ pub struct RowsCounterProps {
23
23
  pub stats: Option<ViewStats>,
24
24
  }
25
25
 
26
- /// A component to show the current [`Table`]'s dimensions.
26
+ /// Shows a panel's `Table`/`View` dimensions in its `PanelTab` header.
27
27
  #[function_component]
28
- pub fn StatusBarRowsCounter(props: &StatusBarRowsCounterProps) -> Html {
28
+ pub fn RowsCounter(props: &RowsCounterProps) -> Html {
29
29
  match props.stats {
30
30
  Some(
31
31
  ViewStats {
@@ -49,7 +49,7 @@ pub fn StatusBarRowsCounter(props: &StatusBarRowsCounterProps) -> Html {
49
49
  <span id="rows">
50
50
  <span>{ vrows }</span>
51
51
  <span class="total">{ format!(" ({})", nrows) }</span>
52
- <span class="x">{ " x " }</span>
52
+ <span class="x">{ " / " }</span>
53
53
  <span>{ vcols }</span>
54
54
  <span class="total">{ format!(" ({})", ncols) }</span>
55
55
  </span>
@@ -77,7 +77,7 @@ pub fn StatusBarRowsCounter(props: &StatusBarRowsCounterProps) -> Html {
77
77
  <span id="rows">
78
78
  <span>{ vrows }</span>
79
79
  <span class="total">{ format!(" ({})", nrows) }</span>
80
- <span class="x">{ " x " }</span>
80
+ <span class="x">{ " / " }</span>
81
81
  <span>{ vcols }</span>
82
82
  </span>
83
83
  }
@@ -94,7 +94,7 @@ pub fn StatusBarRowsCounter(props: &StatusBarRowsCounterProps) -> Html {
94
94
  html! {
95
95
  <span id="rows">
96
96
  <span>{ vrows }</span>
97
- <span class="x">{ " x " }</span>
97
+ <span class="x">{ " / " }</span>
98
98
  <span>{ vcols }</span>
99
99
  <span class="total">{ format!(" ({})", ncols) }</span>
100
100
  </span>
@@ -110,7 +110,7 @@ pub fn StatusBarRowsCounter(props: &StatusBarRowsCounterProps) -> Html {
110
110
  html! {
111
111
  <span id="rows">
112
112
  <span>{ nrows }</span>
113
- <span class="x">{ " x " }</span>
113
+ <span class="x">{ " / " }</span>
114
114
  <span>{ ncols }</span>
115
115
  </span>
116
116
  }
@@ -19,8 +19,7 @@ use yew::prelude::*;
19
19
  use super::column_selector::ColumnSelector;
20
20
  use super::plugin_selector::PluginSelector;
21
21
  use super::plugin_tab::PluginTab;
22
- use crate::components::containers::sidebar_close_button::SidebarCloseButton;
23
- use crate::components::form::debug::DebugPanel;
22
+ use crate::components::debug_panel::DebugPanel;
24
23
  use crate::config::{PluginStaticConfig, PluginUpdate};
25
24
  use crate::presentation::{ColumnLocator, ColumnSettingsTarget, OpenColumnSettings, Presentation};
26
25
  use crate::queries::classify_column;
@@ -28,6 +27,7 @@ use crate::renderer::*;
28
27
  use crate::session::column_defaults_update::*;
29
28
  use crate::session::*;
30
29
  use crate::tasks::update_plugin_and_render;
30
+ use crate::ui::SidebarCloseButton;
31
31
  use crate::utils::*;
32
32
  use crate::workspace::Workspace;
33
33