@perspective-dev/viewer 5.3.1 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/dist/cdn/perspective-viewer.js +6 -6
  2. package/dist/cdn/perspective-viewer.js.map +2 -2
  3. package/dist/css/blueprint.css +1 -1
  4. package/dist/css/botanical.css +1 -1
  5. package/dist/css/dracula.css +1 -1
  6. package/dist/css/eggplant.css +1 -1
  7. package/dist/css/gruvbox-dark.css +1 -1
  8. package/dist/css/gruvbox.css +1 -1
  9. package/dist/css/icons.css +1 -1
  10. package/dist/css/intl/ar.css +1 -0
  11. package/dist/css/intl/de.css +1 -1
  12. package/dist/css/intl/es.css +1 -1
  13. package/dist/css/intl/fr.css +1 -1
  14. package/dist/css/intl/ja.css +1 -1
  15. package/dist/css/intl/pt.css +1 -1
  16. package/dist/css/intl/zh.css +1 -1
  17. package/dist/css/intl.css +1 -1
  18. package/dist/css/ledger.css +1 -1
  19. package/dist/css/monokai.css +1 -1
  20. package/dist/css/nord.css +1 -1
  21. package/dist/css/phosphor.css +1 -1
  22. package/dist/css/pro-dark.css +1 -1
  23. package/dist/css/pro.css +1 -1
  24. package/dist/css/solarized-dark.css +1 -1
  25. package/dist/css/solarized.css +1 -1
  26. package/dist/css/themes.css +1 -1
  27. package/dist/css/vaporwave.css +1 -1
  28. package/dist/css/velvet.css +1 -1
  29. package/dist/docs/perspective-docs.json +1 -1
  30. package/dist/esm/extensions.d.ts +0 -5
  31. package/dist/esm/perspective-viewer.d.ts +2 -0
  32. package/dist/esm/perspective-viewer.inline.js +8 -8
  33. package/dist/esm/perspective-viewer.inline.js.map +2 -2
  34. package/dist/esm/perspective-viewer.js +5 -5
  35. package/dist/esm/perspective-viewer.js.map +2 -2
  36. package/dist/esm/plugin.d.ts +9 -0
  37. package/dist/esm/ts-rs/AddPanelOptions.d.ts +9 -0
  38. package/dist/esm/ts-rs/GetViewMode.d.ts +4 -0
  39. package/dist/esm/ts-rs/GetViewOptions.d.ts +8 -0
  40. package/dist/esm/ts-rs/RestoreOptions.d.ts +6 -0
  41. package/dist/esm/ts-rs/RestoreWorkspaceOptions.d.ts +9 -0
  42. package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +1 -1
  43. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +30 -22
  44. package/dist/wasm/perspective-viewer.d.ts +137 -104
  45. package/dist/wasm/perspective-viewer.js +337 -269
  46. package/dist/wasm/perspective-viewer.wasm +0 -0
  47. package/dist/wasm/perspective-viewer.wasm.d.ts +20 -30
  48. package/package.json +7 -7
  49. package/src/css/_context_menu.css +1 -1
  50. package/src/css/_dropdown_menu.css +1 -1
  51. package/src/css/_viewer_bundle.css +7 -8
  52. package/src/css/column-dropdown.css +0 -3
  53. package/src/css/column-selector.css +18 -7
  54. package/src/css/column-settings-panel.css +0 -225
  55. package/src/css/column-style.css +175 -0
  56. package/src/css/config-selector.css +19 -11
  57. package/src/css/filter-dropdown.css +5 -5
  58. package/src/css/form/debug.css +37 -4
  59. package/src/css/function-dropdown.css +5 -5
  60. package/src/css/panel-tab.css +23 -0
  61. package/src/css/plugin-selector.css +5 -5
  62. package/src/css/render-warning.css +1 -1
  63. package/src/css/status-bar.css +63 -21
  64. package/src/css/type-icon.css +1 -3
  65. package/src/css/{containers → ui/containers}/context-menu.css +22 -16
  66. package/src/css/ui/containers/dropdown-menu-items.css +66 -0
  67. package/src/css/{containers → ui/containers}/dropdown-menu.css +8 -53
  68. package/src/css/{containers → ui/containers}/tabs.css +2 -1
  69. package/src/css/viewer.css +32 -26
  70. package/src/rust/agent/tools.rs +80 -7
  71. package/src/rust/components/chat_panel.rs +1 -1
  72. package/src/rust/components/{form/code_editor.rs → code_editor.rs} +4 -2
  73. package/src/rust/components/column_dropdown.rs +2 -2
  74. package/src/rust/components/column_selector/active_column.rs +24 -9
  75. package/src/rust/components/column_selector/aggregate_selector.rs +1 -1
  76. package/src/rust/components/column_selector/config_selector.rs +2 -2
  77. package/src/rust/components/column_selector/filter_column.rs +8 -5
  78. package/src/rust/components/column_selector/inactive_column.rs +4 -2
  79. package/src/rust/components/column_selector/pivot_column.rs +4 -3
  80. package/src/rust/components/column_selector/sort_column.rs +7 -4
  81. package/src/rust/components/column_selector.rs +14 -3
  82. package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +1 -1
  83. package/src/rust/components/column_settings_sidebar/style_tab/alignment_field.rs +90 -0
  84. package/src/rust/components/column_settings_sidebar/style_tab/font_field.rs +267 -0
  85. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +16 -18
  86. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +1 -1
  87. package/src/rust/components/column_settings_sidebar/style_tab.rs +70 -12
  88. package/src/rust/components/column_settings_sidebar.rs +3 -4
  89. package/src/rust/components/copy_dropdown.rs +1 -1
  90. package/src/rust/components/datetime_column_style/custom.rs +1 -3
  91. package/src/rust/components/datetime_column_style/simple.rs +1 -2
  92. package/src/rust/components/datetime_column_style.rs +1 -2
  93. package/src/rust/components/{form/debug.rs → debug_panel.rs} +112 -55
  94. package/src/rust/components/{containers/dragdrop_list.rs → dragdrop_list.rs} +2 -10
  95. package/src/rust/components/export_dropdown.rs +1 -1
  96. package/src/rust/components/expression_editor.rs +1 -1
  97. package/src/rust/components/filter_dropdown.rs +2 -2
  98. package/src/rust/components/function_dropdown.rs +2 -2
  99. package/src/rust/components/main_panel/frame_theme.rs +3 -59
  100. package/src/rust/components/main_panel/msg.rs +4 -0
  101. package/src/rust/components/main_panel/reconcile.rs +9 -4
  102. package/src/rust/components/main_panel/render.rs +62 -11
  103. package/src/rust/components/main_panel/update.rs +39 -10
  104. package/src/rust/components/main_panel.rs +40 -2
  105. package/src/rust/components/mod.rs +5 -8
  106. package/src/rust/components/new_panel_menu.rs +97 -0
  107. package/src/rust/components/number_series_style.rs +2 -3
  108. package/src/rust/components/panel_menu.rs +61 -87
  109. package/src/rust/components/panel_tab.rs +100 -43
  110. package/src/rust/components/plugin_tab.rs +58 -4
  111. package/src/rust/components/render_warning.rs +2 -2
  112. package/src/rust/components/{status_bar_counter.rs → rows_counter.rs} +10 -10
  113. package/src/rust/components/settings_panel.rs +8 -2
  114. package/src/rust/components/status_bar.rs +113 -19
  115. package/src/rust/components/status_indicator.rs +9 -14
  116. package/src/rust/components/{style/mod.rs → style.rs} +32 -15
  117. package/src/rust/components/style_controls/number_string_format/digits_section.rs +3 -5
  118. package/src/rust/components/style_controls/number_string_format/misc_section.rs +1 -1
  119. package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -1
  120. package/src/rust/components/viewer/msg.rs +43 -30
  121. package/src/rust/components/viewer/panels.rs +53 -19
  122. package/src/rust/components/viewer/render.rs +16 -13
  123. package/src/rust/components/viewer/settings.rs +137 -68
  124. package/src/rust/components/viewer/snapshots.rs +5 -10
  125. package/src/rust/components/viewer/wiring.rs +11 -22
  126. package/src/rust/components/viewer.rs +15 -10
  127. package/src/rust/components/window_editor.rs +8 -9
  128. package/src/rust/config/column_config_schema.rs +312 -33
  129. package/src/rust/config/datetime_column_style.rs +0 -11
  130. package/src/rust/config/mod.rs +0 -4
  131. package/src/rust/config/options.rs +50 -0
  132. package/src/rust/config/workspace_config.rs +70 -24
  133. package/src/rust/custom_elements/debug_plugin.rs +1 -10
  134. package/src/rust/custom_elements/mod.rs +0 -3
  135. package/src/rust/custom_elements/viewer.rs +194 -115
  136. package/src/rust/js/plugin.rs +3 -87
  137. package/src/rust/lib.rs +19 -18
  138. package/src/rust/presentation/column_locator.rs +3 -7
  139. package/src/rust/presentation.rs +68 -38
  140. package/src/rust/queries/hosted_tables.rs +38 -0
  141. package/src/rust/queries/mod.rs +2 -0
  142. package/src/rust/queries/palette_set.rs +1 -1
  143. package/src/rust/queries/plugin_column_styles.rs +6 -5
  144. package/src/rust/renderer/activate.rs +1 -3
  145. package/src/rust/renderer/dispatch.rs +83 -22
  146. package/src/rust/renderer/limits.rs +24 -5
  147. package/src/rust/renderer/plugin_config.rs +201 -41
  148. package/src/rust/renderer/plugin_store.rs +2 -1
  149. package/src/rust/renderer/props.rs +7 -1
  150. package/src/rust/renderer.rs +23 -27
  151. package/src/rust/session/props.rs +8 -3
  152. package/src/rust/session/view_subscription.rs +19 -6
  153. package/src/rust/session.rs +189 -150
  154. package/src/rust/tasks/create_panel.rs +5 -2
  155. package/src/rust/tasks/dismiss_render_warning.rs +1 -1
  156. package/src/rust/tasks/eject.rs +1 -0
  157. package/src/rust/tasks/pipeline.rs +18 -67
  158. package/src/rust/tasks/presize_panels.rs +24 -1
  159. package/src/rust/tasks/restore_and_render.rs +0 -17
  160. package/src/rust/tasks/restore_panel.rs +129 -30
  161. package/src/rust/tasks/send_column_config.rs +4 -1
  162. package/src/rust/tasks/send_plugin_config.rs +1 -1
  163. package/src/rust/tasks/set_edit_mode.rs +1 -1
  164. package/src/rust/tasks/sync_update_panels.rs +147 -67
  165. package/src/rust/tasks/table_lifecycle.rs +7 -1
  166. package/src/rust/{components → ui}/containers/control_group.rs +25 -3
  167. package/src/rust/{components → ui}/containers/dropdown_menu.rs +5 -2
  168. package/src/rust/{components → ui}/containers/mod.rs +0 -4
  169. package/src/rust/{components → ui}/containers/scroll_panel.rs +0 -22
  170. package/src/rust/{components → ui}/containers/scroll_panel_item.rs +0 -13
  171. package/src/rust/{components → ui}/containers/select.rs +0 -6
  172. package/src/rust/{components → ui}/containers/sidebar.rs +5 -32
  173. package/src/rust/{components → ui}/containers/split_panel.rs +8 -80
  174. package/src/rust/{components → ui}/containers/tab_list.rs +15 -9
  175. package/src/rust/{components → ui}/context_menu.rs +8 -13
  176. package/src/rust/{presentation/drag_helpers.rs → ui/dragdrop.rs} +26 -31
  177. package/src/rust/{components → ui}/font_loader.rs +0 -26
  178. package/src/rust/{components → ui}/form/color_selector.rs +4 -2
  179. package/src/rust/{config/datetime_column_style/color_mode.rs → ui/form/intl_label.rs} +42 -42
  180. package/src/rust/{components → ui}/form/mirrored_textarea.rs +0 -53
  181. package/src/rust/{components → ui}/form/mod.rs +1 -10
  182. package/src/rust/{components → ui}/form/multi_stop_gradient_selector.rs +4 -5
  183. package/src/rust/{components → ui}/form/named_value_picker.rs +1 -6
  184. package/src/rust/{components → ui}/form/number_field.rs +1 -1
  185. package/src/rust/{components → ui}/form/number_range_field.rs +1 -1
  186. package/src/rust/{components → ui}/form/optional_field.rs +3 -1
  187. package/src/rust/{components → ui}/form/palette_selector.rs +4 -5
  188. package/src/rust/{components → ui}/form/select_enum_field.rs +2 -2
  189. package/src/rust/{components → ui}/form/select_value_field.rs +2 -2
  190. package/src/rust/ui/mod.rs +56 -0
  191. package/src/rust/ui/modal.rs +61 -0
  192. package/src/rust/{components → ui}/portal.rs +175 -79
  193. package/src/rust/{components/style/style_provider.rs → ui/style.rs} +4 -34
  194. package/src/rust/utils/completion.rs +0 -6
  195. package/src/rust/{config/css_scale.rs → utils/css.rs} +38 -35
  196. package/src/rust/utils/css_vars.rs +13 -0
  197. package/src/rust/utils/local_fonts.rs +395 -0
  198. package/src/rust/utils/mod.rs +4 -0
  199. package/src/rust/workspace.rs +47 -0
  200. package/src/svg/boolean-type.svg +3 -2
  201. package/src/svg/checkbox-checked-icon.svg +4 -7
  202. package/src/svg/checkbox-hover.svg +3 -3
  203. package/src/svg/checkbox-off.svg +2 -3
  204. package/src/svg/checkbox-on.svg +3 -3
  205. package/src/svg/checkbox-unchecked-icon.svg +3 -3
  206. package/src/svg/datagrid-select-column.svg +8 -15
  207. package/src/svg/datagrid-select-region.svg +8 -15
  208. package/src/svg/datagrid-select-row-tree.svg +7 -12
  209. package/src/svg/datagrid-select-row.svg +8 -13
  210. package/src/svg/date-type.svg +4 -23
  211. package/src/svg/drawer-tab-hover.svg +4 -6
  212. package/src/svg/drawer-tab-invert-hover.svg +5 -5
  213. package/src/svg/drawer-tab-invert.svg +3 -5
  214. package/src/svg/drawer-tab.svg +5 -5
  215. package/src/svg/mega-menu-icons-candlestick.svg +5 -37
  216. package/src/svg/mega-menu-icons-ohlc.svg +4 -39
  217. package/src/svg/mega-menu-icons-sunburst.svg +4 -38
  218. package/src/svg/mega-menu-icons-treemap.svg +3 -43
  219. package/src/svg/mega-menu-icons-y-scatter.svg +12 -72
  220. package/src/svg/new-panel-icon.svg +4 -0
  221. package/src/svg/number-type.svg +3 -3
  222. package/src/svg/radio-hover.svg +3 -3
  223. package/src/svg/radio-off.svg +3 -3
  224. package/src/svg/radio-on.svg +3 -3
  225. package/src/svg/status_ok.svg +3 -26
  226. package/src/svg/status_pending.svg +3 -0
  227. package/src/svg/string-type.svg +4 -8
  228. package/src/svg/theme-icon.svg +5 -5
  229. package/src/themes/blueprint.css +0 -57
  230. package/src/themes/botanical.css +0 -36
  231. package/src/themes/defaults.css +1 -24
  232. package/src/themes/dracula.css +0 -58
  233. package/src/themes/eggplant.css +0 -58
  234. package/src/themes/gruvbox-dark.css +0 -65
  235. package/src/themes/gruvbox.css +1 -39
  236. package/src/themes/icons.css +2 -8
  237. package/src/themes/intl/ar.css +198 -0
  238. package/src/themes/intl/de.css +63 -40
  239. package/src/themes/intl/es.css +58 -35
  240. package/src/themes/intl/fr.css +57 -34
  241. package/src/themes/intl/ja.css +59 -36
  242. package/src/themes/intl/pt.css +54 -31
  243. package/src/themes/intl/zh.css +65 -42
  244. package/src/themes/intl.css +34 -8
  245. package/src/themes/ledger.css +1 -59
  246. package/src/themes/monokai.css +0 -51
  247. package/src/themes/nord.css +0 -55
  248. package/src/themes/phosphor.css +0 -48
  249. package/src/themes/pro-dark.css +0 -37
  250. package/src/themes/pro.css +3 -23
  251. package/src/themes/solarized-dark.css +0 -31
  252. package/src/themes/solarized.css +1 -24
  253. package/src/themes/vaporwave.css +0 -44
  254. package/src/themes/velvet.css +0 -60
  255. package/src/ts/extensions.ts +0 -13
  256. package/src/ts/perspective-viewer.ts +2 -0
  257. package/src/ts/plugin.ts +9 -0
  258. package/src/ts/ts-rs/AddPanelOptions.ts +10 -0
  259. package/src/ts/ts-rs/{DatetimeColorMode.ts → GetViewMode.ts} +4 -1
  260. package/src/ts/ts-rs/GetViewOptions.ts +7 -0
  261. package/src/ts/ts-rs/RestoreOptions.ts +7 -1
  262. package/src/ts/ts-rs/RestoreWorkspaceOptions.ts +10 -0
  263. package/src/ts/ts-rs/ViewOnUpdateResp.ts +1 -1
  264. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +31 -20
  265. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +0 -1
  266. package/dist/esm/ts-rs/FormatMode.d.ts +0 -1
  267. package/dist/esm/ts-rs/StringColorMode.d.ts +0 -1
  268. package/src/css/plugin-settings-panel.css +0 -171
  269. package/src/rust/components/modal.rs +0 -177
  270. package/src/rust/components/string_column_style.rs +0 -121
  271. package/src/rust/config/string_column_style.rs +0 -135
  272. package/src/rust/custom_elements/copy_dropdown.rs +0 -219
  273. package/src/rust/custom_elements/export_dropdown.rs +0 -216
  274. package/src/rust/custom_elements/modal.rs +0 -299
  275. package/src/ts/ts-rs/FormatMode.ts +0 -3
  276. package/src/ts/ts-rs/StringColorMode.ts +0 -3
  277. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline0.js +0 -0
  278. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline1.js +0 -0
  279. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline2.js +0 -0
  280. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline3.js +0 -0
  281. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline4.js +0 -0
  282. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-ea71a3990cad35d7}/inline5.js +0 -0
  283. /package/src/css/{containers → ui/containers}/scroll-panel.css +0 -0
  284. /package/src/css/{containers → ui/containers}/split-panel.css +0 -0
  285. /package/src/css/{dom → ui/dom}/checkbox.css +0 -0
  286. /package/src/css/{dom → ui/dom}/scrollbar.css +0 -0
  287. /package/src/css/{dom → ui/dom}/select.css +0 -0
  288. /package/src/css/{form → ui/form}/mirrored-textarea.css +0 -0
  289. /package/src/rust/components/{form → code_editor}/highlight.rs +0 -0
  290. /package/src/rust/{components → ui}/containers/sidebar_close_button.rs +0 -0
  291. /package/src/rust/{components → ui}/form/number_input.rs +0 -0
@@ -10,21 +10,6 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- //! The render pipeline (see `SESSION_CONFIG_COHERENCE_PLAN.md`) — the ONE
14
- //! review surface for how config-driven renders happen.
15
- //!
16
- //! Invariants enforced here and by the types this composes:
17
- //! - **I1** config mutation is a synchronous commit
18
- //! ([`Session::commit_view_config`]) — no async writer exists.
19
- //! - **I2** every run consumes an immutable snapshot taken inside the draw
20
- //! lock; async stages never re-read the live config (the `ValidatedSnapshot`
21
- //! type-state token).
22
- //! - **I3** runs serialize on the per-`Renderer` draw lock (the [`RenderGuard`]
23
- //! witness — plugin dispatch without it does not compile) and every commit
24
- //! schedules a run, so the final render always reflects the final commit.
25
- //! - **I5** the run pins a [`RenderContext`] so plugin read-backs mid-render
26
- //! answer from the run's snapshot, never live state.
27
-
28
13
  use std::rc::Rc;
29
14
 
30
15
  use futures::future::LocalBoxFuture;
@@ -79,30 +64,10 @@ pub async fn bind_snapshot(
79
64
  /// legitimate concurrent public calls).
80
65
  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
81
66
  pub enum RunOrigin {
82
- /// A public element API entry (`restore`, `restorePanel`, `reset`,
83
- /// `load`, …): an explicit external request, so a run that reconciles
84
- /// `Unchanged` with no state delta STILL repaints — the documented
85
- /// no-op-restore refresh affordance (`update` source 6, "the user
86
- /// asked" — `PLUGIN_DRAW_INVARIANT_PLAN.md` amendment).
87
67
  Public,
88
- /// A host-initiated run (UI commits, unpause resumes, subscriptions):
89
- /// a reconciled no-op dispatches nothing.
90
68
  Internal,
91
69
  }
92
70
 
93
- /// The ONE decision table mapping a run's [`BindDisposition`] to its plugin
94
- /// dispatch (`PLUGIN_DRAW_INVARIANT_PLAN.md`, as amended 2026-07-16):
95
- /// `plugin.draw` fires iff there is a NEW `View` — a REBUILD, or a
96
- /// freshly-selected plugin's first paint of the bound `View`
97
- /// (`promote_first_paint`). `plugin.update` fires iff the `View` is
98
- /// unchanged but a plugin-visible SOURCE changed: the `Adopted`
99
- /// config delta, a genuinely changed plugin/columns bucket this run
100
- /// delivered via `plugin.restore` (`plugin_state_changed`), or an explicit
101
- /// public API request ([`RunOrigin::Public`]). An INTERNAL run that
102
- /// reconciled `Unchanged` with no state delta dispatches NOTHING — no
103
- /// defensive repaints (this is what makes an inert plugin echo, e.g. the
104
- /// datagrid's `toggle_edit_mode` `restorePanel`, render-silent). Deferred
105
- /// binds dispatch nothing.
106
71
  pub async fn dispatch_bound(
107
72
  guard: &RenderGuard,
108
73
  renderer: &Renderer,
@@ -286,6 +251,18 @@ pub(crate) async fn locked_run(
286
251
  let view_config_snapshot = session.get_view_config().clone();
287
252
  let plugin_config_changed =
288
253
  renderer.update_plugin_config(&view_config_snapshot, spec.plugin_config)?;
254
+ let plugin_config_snapshot = renderer.get_plugin_config();
255
+ let plugin_update =
256
+ JsValue::from_serde_ext(&plugin_config_snapshot).unwrap_or(JsValue::NULL);
257
+
258
+ let plugin_restored = plugin_config_changed || plugin_swapped;
259
+ if plugin_restored {
260
+ let columns_config = renderer
261
+ .all_columns_configs_materialized(&view_config_snapshot, &session)
262
+ .await;
263
+ plugin.restore(&plugin_update, Some(&columns_config))?;
264
+ }
265
+
289
266
  let columns_config_changed = renderer.update_columns_configs(
290
267
  &view_config_snapshot,
291
268
  &session,
@@ -293,17 +270,16 @@ pub(crate) async fn locked_run(
293
270
  )?;
294
271
 
295
272
  let changed = plugin_config_changed || columns_config_changed;
296
- if changed || plugin_swapped {
297
- let plugin_config_snapshot = renderer.get_plugin_config();
298
- let plugin_update =
299
- JsValue::from_serde_ext(&plugin_config_snapshot).unwrap_or(JsValue::NULL);
273
+ if columns_config_changed {
300
274
  let columns_config = renderer
301
275
  .all_columns_configs_materialized(&view_config_snapshot, &session)
302
276
  .await;
303
277
  plugin.restore(&plugin_update, Some(&columns_config))?;
304
- if plugin_config_changed {
305
- renderer.plugin_config_changed.emit(plugin_config_snapshot);
306
- }
278
+ renderer.columns_config_changed.emit(columns_config);
279
+ }
280
+
281
+ if plugin_config_changed {
282
+ renderer.plugin_config_changed.emit(plugin_config_snapshot);
307
283
  }
308
284
 
309
285
  if spec
@@ -343,31 +319,6 @@ async fn render_run(
343
319
  }
344
320
  }
345
321
 
346
- /// One TRANSACTIONAL activation repaint (see the I5 audit gap in
347
- /// `SESSION_CONFIG_COHERENCE_PLAN.md` §4): under the panel's draw lock,
348
- /// stamp the `active` class + theme and `plugin.resize()` in ONE dispatch,
349
- /// so the activation-dependent chrome (e.g. the datagrid's edit
350
- /// column-header row, decided by its style listener reading that class) and
351
- /// the class that styles it land in a single paint commit, never split.
352
- ///
353
- /// `resize`, NOT `draw`/`update`: activation creates no new `View` and
354
- /// changes no data (`plugin.draw` ⇔ new `View` —
355
- /// `PLUGIN_DRAW_INVARIANT_PLAN.md`), and a full dispatch on charts is a
356
- /// fetch + multi-blit repaint — the stacked-tab two-stage regression, paid
357
- /// by BOTH sides of every tab switch. `resize` is each plugin's cheap
358
- /// repaint-from-retained-state, and both built-ins skip it while hidden, so
359
- /// the OUTGOING (unslotted) panel's nudge costs nothing. Deliberately NOT
360
- /// debounced/throttled: activation is a one-shot interaction, not a data
361
- /// stream, and the throttle timer is a task boundary the browser paints
362
- /// across.
363
- ///
364
- /// EXCEPTION: a panel whose `table_updated` redraws were dropped while it
365
- /// was hidden ([`Renderer::take_data_stale`]) HAS changed data — its
366
- /// retained frame is stale, so this activation dispatches one full
367
- /// `plugin.update` (same `View`, fresh data; `draw` stays reserved for new
368
- /// `View`s) instead of the chrome-only repaint. `draw_view` stamps the
369
- /// activation class + theme itself, so the atomicity contract above holds
370
- /// on this branch too.
371
322
  pub async fn activation_render(session: Session, renderer: Renderer) -> ApiResult<()> {
372
323
  let result = {
373
324
  clone!(session, renderer);
@@ -166,13 +166,14 @@ pub async fn presize_visible_panels_open(
166
166
  pub async fn presize_visible_panels_pane_width(
167
167
  workspace: &Workspace,
168
168
  elem: &web_sys::HtmlElement,
169
+ pane_sel: &str,
169
170
  pane_width: f64,
170
171
  ) -> StagedPresents {
171
172
  let Some(mpc) = shadow_rect(elem, "#main_panel_container") else {
172
173
  return StagedPresents::default();
173
174
  };
174
175
 
175
- let Some(pane) = shadow_rect(elem, "#app_panel > .split-panel-child") else {
176
+ let Some(pane) = shadow_rect(elem, pane_sel) else {
176
177
  return StagedPresents::default();
177
178
  };
178
179
 
@@ -308,6 +309,28 @@ fn shadow_rect(elem: &web_sys::HtmlElement, sel: &str) -> Option<web_sys::DomRec
308
309
  )
309
310
  }
310
311
 
312
+ /// The width the `SplitPanel` pane at `pane_sel` takes with no divider
313
+ /// override, measured under one forced synchronous layout that no paint or
314
+ /// `ResizeObserver` can observe, or `None` when the pane isn't in the DOM.
315
+ pub fn measure_pane_natural_width(elem: &web_sys::HtmlElement, pane_sel: &str) -> Option<f64> {
316
+ let pane = elem
317
+ .shadow_root()?
318
+ .query_selector(pane_sel)
319
+ .ok()??
320
+ .dyn_into::<web_sys::HtmlElement>()
321
+ .ok()?;
322
+
323
+ let style = pane.style();
324
+ let css_text = style.css_text();
325
+ let class_name = pane.class_name();
326
+ style.set_css_text("");
327
+ let _ = pane.class_list().remove_1("is-width-override");
328
+ let width = pane.get_bounding_client_rect().width();
329
+ style.set_css_text(&css_text);
330
+ pane.set_class_name(&class_name);
331
+ Some(width)
332
+ }
333
+
311
334
  /// The open-state geometry deltas cached for the next settings *open* (P2):
312
335
  /// `(layout_area.w − mpc.w, main_column.h − mpc.h)`. Only meaningful while the
313
336
  /// settings pane is open (i.e. measured at close time).
@@ -10,10 +10,6 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- //! Apply a full [`ViewerConfigUpdate`] (settings, theme, title, plugin,
14
- //! plugin_config, columns_config, view_config) and re-draw, on the snapshot
15
- //! pipeline (see `tasks/pipeline.rs` / `SESSION_CONFIG_COHERENCE_PLAN.md`).
16
-
17
13
  use futures::Future;
18
14
  use perspective_client::clone;
19
15
 
@@ -28,19 +24,6 @@ use crate::*;
28
24
  /// Apply a full [`ViewerConfigUpdate`] (theme, title, plugin selection,
29
25
  /// plugin config, columns config, view config) to the engines and re-draw.
30
26
  /// Returns an [`ApiFuture<()>`] which resolves when the draw completes.
31
- ///
32
- /// `origin` says who initiated the restore ([`RunOrigin`]): a `Public`
33
- /// element-API call keeps the no-op-restore refresh affordance (an
34
- /// `Unchanged` reconcile still repaints via `update`); an `Internal`
35
- /// restore that reconciles `Unchanged` and changes no plugin state
36
- /// dispatches nothing.
37
- ///
38
- /// This function owns the PRE-LOCK prologue only (element-level settings /
39
- /// title / host-theme mirror, plugin resolution, the synchronous config
40
- /// commit and spinner token); the run itself is
41
- /// [`locked_run`] with the caller's `task` awaited inside the lock. The
42
- /// `update`'s `table` field is NOT applied here — table binding is the
43
- /// `task`'s job (see `restore_panel` / `table_lifecycle`).
44
27
  pub fn restore_and_render(
45
28
  session: &Session,
46
29
  renderer: &Renderer,
@@ -16,7 +16,7 @@ use perspective_client::utils::PerspectiveResultExt;
16
16
  use crate::config::*;
17
17
  use crate::presentation::Presentation;
18
18
  use crate::renderer::Renderer;
19
- use crate::session::{ResetOptions, Session};
19
+ use crate::session::{MissingTable, ResetOptions, Session, TableIntermediateState, probe_table};
20
20
  use crate::tasks::*;
21
21
  use crate::workspace::Workspace;
22
22
  use crate::*;
@@ -38,27 +38,107 @@ pub(crate) enum RestoreErrors {
38
38
  Suppress,
39
39
  }
40
40
 
41
+ /// The client to bind and the opened `Table`, or `None` when pending.
42
+ type Probed = (
43
+ perspective_client::Client,
44
+ Option<perspective_client::Table>,
45
+ );
46
+
47
+ /// Resolve and probe the client hosting `name` without touching the
48
+ /// session's own binding.
49
+ async fn probe(
50
+ session: &Session,
51
+ workspace: &Workspace,
52
+ name: &str,
53
+ missing: MissingTable,
54
+ ) -> ApiResult<Probed> {
55
+ let current = session.get_client();
56
+ let resolved = workspace
57
+ .resolve_client_for_table(name, current.as_ref())
58
+ .await;
59
+
60
+ let client = resolved.or(current).into_apierror()?;
61
+ let table = probe_table(&client, name, missing).await?;
62
+ Ok((client, table))
63
+ }
64
+
65
+ /// Bind an unbound or same-named session to `name`.
41
66
  pub(crate) async fn bind_table_task(
42
67
  session: &Session,
43
68
  workspace: &Workspace,
44
69
  name: String,
70
+ missing: MissingTable,
45
71
  ) -> ApiResult<()> {
46
- let current = session.get_client();
47
- if let Some(client) = workspace
48
- .resolve_client_for_table(&name, current.as_ref())
49
- .await
72
+ if session
73
+ .get_table()
74
+ .is_some_and(|t| t.get_name() == name.as_str())
50
75
  {
51
- session.set_client(client);
76
+ return Ok(());
77
+ }
78
+
79
+ let (client, table) = probe(session, workspace, &name, missing).await?;
80
+ session.set_client(client);
81
+ match table {
82
+ Some(table) => session.bind_table(table).await?,
83
+ None => session.pend_table(name).await?,
84
+ }
85
+
86
+ session.commit_table_defaults();
87
+ Ok(())
88
+ }
89
+
90
+ /// Rebind a session to `name`, probing the incoming table before the
91
+ /// outgoing binding is dropped and replaying the journal opened as
92
+ /// `generation` over the incoming table's defaults.
93
+ async fn rebind_table_task(
94
+ session: &Session,
95
+ renderer: &Renderer,
96
+ workspace: &Workspace,
97
+ name: String,
98
+ missing: MissingTable,
99
+ generation: u32,
100
+ ) -> ApiResult<()> {
101
+ let probed = probe(session, workspace, &name, missing).await;
102
+ let journal = session.take_pending_load(generation);
103
+ let (client, table) = probed?;
104
+ let Some(journal) = journal else {
105
+ return Ok(());
106
+ };
107
+
108
+ session
109
+ .reset(ResetOptions {
110
+ config: true,
111
+ expressions: true,
112
+ stats: true,
113
+ table: Some(TableIntermediateState::Reloaded),
114
+ })
115
+ .await?;
116
+
117
+ session.set_client(client);
118
+ match table {
119
+ Some(table) => session.bind_table(table).await?,
120
+ None => session.pend_table(name).await?,
52
121
  }
53
122
 
54
- session.set_table(name).await?;
55
123
  session.commit_table_defaults();
124
+ for delta in journal {
125
+ session.commit_view_config(delta)?;
126
+ }
127
+
128
+ session.commit_table_defaults();
129
+ if session.get_table().is_none()
130
+ && let Some(plugin) = renderer.active_plugin()
131
+ {
132
+ plugin.clear().await?;
133
+ }
134
+
56
135
  Ok(())
57
136
  }
58
137
 
59
138
  /// Apply a [`ViewerConfigUpdate`] to a single panel and re-draw — the one
60
139
  /// pipeline shared by `restorePanel` (an existing panel), `restoreWorkspace`,
61
140
  /// and `addPanel` (both fresh panels).
141
+ #[allow(clippy::too_many_arguments)]
62
142
  pub(crate) async fn restore_panel(
63
143
  session: &Session,
64
144
  renderer: &Renderer,
@@ -67,6 +147,7 @@ pub(crate) async fn restore_panel(
67
147
  mode: RestoreMode,
68
148
  mut update: ViewerConfigUpdate,
69
149
  errors: RestoreErrors,
150
+ missing: MissingTable,
70
151
  ) -> ApiResult<()> {
71
152
  let active = matches!(mode, RestoreMode::Existing { active: true });
72
153
  let fresh = matches!(mode, RestoreMode::Fresh);
@@ -100,21 +181,24 @@ pub(crate) async fn restore_panel(
100
181
  let rollback =
101
182
  matches!(errors, RestoreErrors::Suppress).then(|| session.get_view_config().clone());
102
183
 
103
- let table_changed = !fresh
104
- && matches!(&update.table, OptionalUpdate::Update(name)
105
- if session.get_table().map(|t| t.get_name() != name.as_str()).unwrap_or(true));
184
+ let binding_before = (
185
+ session.get_table().map(|t| t.get_name().to_owned()),
186
+ session.pending_table(),
187
+ );
106
188
 
107
- let errored_recovery =
108
- session.is_errored() && matches!(&update.table, OptionalUpdate::Update(_));
109
-
110
- let reset = (table_changed || errored_recovery).then(|| {
111
- session.reset(ResetOptions {
112
- config: true,
113
- expressions: true,
114
- stats: true,
115
- ..ResetOptions::default()
116
- })
117
- });
189
+ let generation = match &update.table {
190
+ OptionalUpdate::Update(name)
191
+ if session.is_errored()
192
+ || (!fresh
193
+ && session
194
+ .get_table()
195
+ .map(|t| t.get_name() != name.as_str())
196
+ .unwrap_or(true)) =>
197
+ {
198
+ Some(session.begin_pending_load())
199
+ },
200
+ _ => None,
201
+ };
118
202
 
119
203
  let result = restore_and_render(
120
204
  session,
@@ -123,27 +207,42 @@ pub(crate) async fn restore_panel(
123
207
  RunOrigin::Public,
124
208
  update.clone(),
125
209
  {
126
- clone!(session, update.table, workspace);
210
+ clone!(session, renderer, update.table, workspace);
127
211
  async move {
128
- if let OptionalUpdate::Update(name) = table {
129
- if let Some(reset) = reset {
130
- reset.await?;
131
- }
212
+ let OptionalUpdate::Update(name) = table else {
213
+ return Ok(());
214
+ };
132
215
 
133
- bind_table_task(&session, &workspace, name).await?;
216
+ match generation {
217
+ Some(generation) => {
218
+ rebind_table_task(
219
+ &session, &renderer, &workspace, name, missing, generation,
220
+ )
221
+ .await
222
+ },
223
+ None => bind_table_task(&session, &workspace, name, missing).await,
134
224
  }
135
-
136
- Ok(())
137
225
  }
138
226
  },
139
227
  )
140
228
  .await;
141
229
 
230
+ if let Some(generation) = generation {
231
+ session.take_pending_load(generation);
232
+ }
233
+
142
234
  if let Err(e) = &result {
143
235
  match errors {
144
236
  RestoreErrors::Publish => session.set_error(false, e.clone()).await?,
145
237
  RestoreErrors::Suppress => {
146
- if let Some(config) = rollback {
238
+ let binding_after = (
239
+ session.get_table().map(|t| t.get_name().to_owned()),
240
+ session.pending_table(),
241
+ );
242
+
243
+ if let Some(config) = rollback
244
+ && binding_after == binding_before
245
+ {
147
246
  session.commit_view_config(config.into()).unwrap_or_log();
148
247
  }
149
248
  },
@@ -49,8 +49,11 @@ pub fn send_column_config(
49
49
 
50
50
  clone!(session);
51
51
  renderer
52
- .update_lazy(async move { Ok(session.get_view()) })
52
+ .update_lazy(async move { Ok(session.get_view_with_dimensions()) })
53
53
  .await?;
54
+ renderer
55
+ .columns_config_changed
56
+ .emit(columns_configs.clone());
54
57
  renderer.column_style_changed.emit(columns_configs);
55
58
  Ok(())
56
59
  })
@@ -44,7 +44,7 @@ pub fn send_plugin_config(session: &Session, renderer: &Renderer, update: Column
44
44
  .restore(&plugin_token, Some(&columns_configs))?;
45
45
  clone!(session);
46
46
  renderer
47
- .update_lazy(async move { Ok(session.get_view()) })
47
+ .update_lazy(async move { Ok(session.get_view_with_dimensions()) })
48
48
  .await?;
49
49
  renderer.plugin_config_changed.emit(plugin_config);
50
50
  }
@@ -51,7 +51,7 @@ pub fn set_edit_mode(session: &Session, renderer: &Renderer, mode: &str) {
51
51
  .restore(&plugin_token, Some(&columns_configs))?;
52
52
  clone!(session);
53
53
  renderer
54
- .update_lazy(async move { Ok(session.get_view()) })
54
+ .update_lazy(async move { Ok(session.get_view_with_dimensions()) })
55
55
  .await?;
56
56
  renderer.plugin_config_changed.emit(plugin_config);
57
57
  }