@perspective-dev/viewer 4.5.2 → 5.0.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 (178) hide show
  1. package/dist/cdn/perspective-viewer.js +20 -1
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/monokai.css +1 -1
  9. package/dist/css/phosphor.css +1 -1
  10. package/dist/css/pro-dark.css +1 -1
  11. package/dist/css/pro.css +1 -1
  12. package/dist/css/solarized-dark.css +1 -1
  13. package/dist/css/solarized.css +1 -1
  14. package/dist/css/themes.css +1 -1
  15. package/dist/css/vaporwave.css +1 -1
  16. package/dist/esm/extensions.d.ts +22 -3
  17. package/dist/esm/perspective-viewer.d.ts +9 -0
  18. package/dist/esm/perspective-viewer.inline.js +20 -1
  19. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  20. package/dist/esm/perspective-viewer.js +20 -1
  21. package/dist/esm/perspective-viewer.js.map +4 -4
  22. package/dist/esm/plugin.d.ts +71 -5
  23. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  24. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  25. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  26. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  27. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  28. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  29. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  30. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  31. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
  32. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  33. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  34. package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
  35. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  36. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  37. package/dist/wasm/perspective-viewer.d.ts +211 -77
  38. package/dist/wasm/perspective-viewer.js +528 -180
  39. package/dist/wasm/perspective-viewer.wasm +0 -0
  40. package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
  41. package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
  42. package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
  43. package/package.json +3 -2
  44. package/src/css/_context_menu.css +14 -0
  45. package/src/css/column-settings-panel.css +1 -1
  46. package/src/css/containers/context-menu.css +101 -0
  47. package/src/css/panel-tab.css +193 -0
  48. package/src/css/plugin-selector.css +7 -10
  49. package/src/css/status-bar.css +75 -81
  50. package/src/css/viewer.css +180 -70
  51. package/src/rust/components/column_dropdown.rs +40 -30
  52. package/src/rust/components/column_selector/active_column.rs +7 -18
  53. package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
  54. package/src/rust/components/column_selector/config_selector.rs +33 -81
  55. package/src/rust/components/column_selector/filter_column.rs +5 -15
  56. package/src/rust/components/column_selector/inactive_column.rs +3 -8
  57. package/src/rust/components/column_selector/sort_column.rs +3 -6
  58. package/src/rust/components/column_selector.rs +5 -11
  59. package/src/rust/components/containers/split_panel.rs +175 -62
  60. package/src/rust/components/context_menu.rs +130 -0
  61. package/src/rust/components/form/debug.rs +3 -0
  62. package/src/rust/components/global_filter_bar.rs +76 -0
  63. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  64. package/src/rust/components/main_panel/msg.rs +62 -0
  65. package/src/rust/components/main_panel/presize.rs +220 -0
  66. package/src/rust/components/main_panel/reconcile.rs +224 -0
  67. package/src/rust/components/main_panel/render.rs +385 -0
  68. package/src/rust/components/main_panel/update.rs +184 -0
  69. package/src/rust/components/main_panel.rs +311 -71
  70. package/src/rust/components/mod.rs +4 -0
  71. package/src/rust/components/panel_menu.rs +486 -0
  72. package/src/rust/components/panel_tab.rs +505 -0
  73. package/src/rust/components/plugin_tab.rs +1 -1
  74. package/src/rust/components/settings_panel.rs +13 -5
  75. package/src/rust/components/status_bar.rs +110 -178
  76. package/src/rust/components/status_indicator.rs +6 -9
  77. package/src/rust/components/style/mod.rs +5 -0
  78. package/src/rust/components/style/style_provider.rs +2 -0
  79. package/src/rust/components/viewer/filters.rs +133 -0
  80. package/src/rust/components/viewer/msg.rs +180 -0
  81. package/src/rust/components/viewer/panels.rs +326 -0
  82. package/src/rust/components/viewer/render.rs +243 -0
  83. package/src/rust/components/viewer/settings.rs +402 -0
  84. package/src/rust/components/viewer/snapshots.rs +134 -0
  85. package/src/rust/components/viewer/wiring.rs +505 -0
  86. package/src/rust/components/viewer.rs +243 -756
  87. package/src/rust/config/export_method.rs +1 -1
  88. package/src/rust/config/mod.rs +4 -0
  89. package/src/rust/config/options.rs +71 -0
  90. package/src/rust/config/viewer_config.rs +24 -2
  91. package/src/rust/config/workspace_config.rs +92 -0
  92. package/src/rust/custom_elements/copy_dropdown.rs +3 -1
  93. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  94. package/src/rust/custom_elements/export_dropdown.rs +3 -1
  95. package/src/rust/custom_elements/viewer.rs +1015 -286
  96. package/src/rust/custom_events.rs +212 -61
  97. package/src/rust/js/mod.rs +2 -0
  98. package/src/rust/js/plugin.rs +297 -10
  99. package/src/rust/js/regular_layout.rs +258 -0
  100. package/src/rust/lib.rs +20 -1
  101. package/src/rust/presentation.rs +66 -33
  102. package/src/rust/queries/get_viewer_config.rs +29 -11
  103. package/src/rust/queries/plugin_column_styles.rs +2 -2
  104. package/src/rust/renderer/activate.rs +41 -14
  105. package/src/rust/renderer/dispatch.rs +436 -0
  106. package/src/rust/renderer/plugin_config.rs +495 -0
  107. package/src/rust/renderer/registry.rs +18 -6
  108. package/src/rust/renderer/render_timer.rs +29 -10
  109. package/src/rust/renderer.rs +350 -837
  110. package/src/rust/session/column_defaults_update.rs +26 -8
  111. package/src/rust/session/view_subscription.rs +30 -15
  112. package/src/rust/session.rs +753 -264
  113. package/src/rust/tasks/apply_global_filters.rs +89 -0
  114. package/src/rust/tasks/auto_pause.rs +171 -0
  115. package/src/rust/tasks/copy_export.rs +29 -19
  116. package/src/rust/tasks/create_panel.rs +243 -0
  117. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  118. package/src/rust/tasks/edit_expression.rs +4 -4
  119. package/src/rust/tasks/eject.rs +64 -23
  120. package/src/rust/tasks/mod.rs +28 -4
  121. package/src/rust/tasks/pipeline.rs +411 -0
  122. package/src/rust/tasks/presize_panels.rs +324 -0
  123. package/src/rust/tasks/reset_all.rs +22 -12
  124. package/src/rust/tasks/resize_observer.rs +124 -87
  125. package/src/rust/tasks/restore_and_render.rs +80 -86
  126. package/src/rust/tasks/restore_panel.rs +145 -0
  127. package/src/rust/tasks/send_column_config.rs +5 -2
  128. package/src/rust/tasks/send_plugin_config.rs +5 -2
  129. package/src/rust/tasks/set_edit_mode.rs +59 -0
  130. package/src/rust/tasks/sync_update_panels.rs +152 -0
  131. package/src/rust/tasks/table_lifecycle.rs +158 -0
  132. package/src/rust/tasks/update_theme.rs +56 -12
  133. package/src/rust/utils/completion.rs +56 -0
  134. package/src/rust/utils/custom_element.rs +5 -0
  135. package/src/rust/utils/debounce.rs +130 -7
  136. package/src/rust/utils/mod.rs +6 -0
  137. package/src/rust/utils/pending_effects.rs +62 -0
  138. package/src/rust/utils/spawn.rs +33 -0
  139. package/src/rust/utils/weak_scope.rs +0 -10
  140. package/src/rust/workspace.rs +846 -0
  141. package/src/svg/active-indicator.svg +3 -0
  142. package/src/themes/botanical.css +5 -2
  143. package/src/themes/defaults.css +54 -2
  144. package/src/themes/dracula.css +6 -3
  145. package/src/themes/gruvbox-dark.css +5 -2
  146. package/src/themes/gruvbox.css +5 -2
  147. package/src/themes/icons.css +4 -2
  148. package/src/themes/monokai.css +5 -2
  149. package/src/themes/phosphor.css +5 -2
  150. package/src/themes/pro-dark.css +5 -2
  151. package/src/themes/pro.css +2 -1
  152. package/src/themes/solarized-dark.css +5 -2
  153. package/src/themes/solarized.css +5 -2
  154. package/src/themes/vaporwave.css +5 -2
  155. package/src/ts/bootstrap.ts +8 -7
  156. package/src/ts/extensions.ts +30 -2
  157. package/src/ts/perspective-viewer.ts +10 -0
  158. package/src/ts/plugin.ts +72 -5
  159. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  160. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  161. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  162. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  163. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  164. package/src/ts/ts-rs/Layout.ts +9 -0
  165. package/src/ts/ts-rs/Orientation.ts +6 -0
  166. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  167. package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
  168. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  169. package/src/ts/ts-rs/TabLayout.ts +11 -0
  170. package/src/ts/ts-rs/ViewerConfig.ts +6 -2
  171. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  172. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  173. package/src/rust/tasks/intersection_observer.rs +0 -98
  174. package/src/rust/tasks/update_and_render.rs +0 -110
  175. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
  176. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
  177. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
  178. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
@@ -33,15 +33,6 @@ pub use self::drag_helpers::{DragDropContainer, DragEndCallback};
33
33
  pub use self::props::{DragDropProps, PresentationProps};
34
34
  use crate::utils::*;
35
35
 
36
- /// The available themes as detected in the browser environment or set
37
- /// explicitly when CORS prevents detection. Detection is expensive and
38
- /// typically must be performed only once, when `document.styleSheets` is
39
- /// up-to-date.
40
- #[derive(Default)]
41
- struct ThemeData {
42
- themes: Option<Vec<String>>,
43
- }
44
-
45
36
  #[derive(Clone, Debug)]
46
37
  struct DragFrom {
47
38
  column: String,
@@ -71,7 +62,23 @@ impl DragState {
71
62
  /// Actual presentations tate struct with some fields hidden.
72
63
  pub struct PresentationHandle {
73
64
  viewer_elem: HtmlElement,
74
- theme_data: Mutex<ThemeData>,
65
+
66
+ /// The available themes as detected in the browser environment or set
67
+ /// explicitly when CORS prevents detection — a MEMO of a document
68
+ /// external, not component state. `None` until first parsed (detection
69
+ /// is expensive and must wait for `document.styleSheets`). Sync-readable
70
+ /// (a `RefCell`, never borrowed across an await) so synchronous paths
71
+ /// ("stamp with commit" — panel creation, theme mutation sites) can
72
+ /// derive the registry default without awaiting registry init; derived
73
+ /// values (e.g. the default theme = first registered) are computed from
74
+ /// it on demand, never mirrored.
75
+ themes: RefCell<Option<Vec<String>>>,
76
+
77
+ /// Single-flight guard for the stylesheet parse that populates
78
+ /// [`Self::themes`] (and exclusion against `reset_available_themes`) —
79
+ /// concurrent `get_available_themes` calls await one parse instead of
80
+ /// racing their own.
81
+ theme_init: Mutex<()>,
75
82
  is_settings_open: RefCell<bool>,
76
83
  open_column_settings: RefCell<OpenColumnSettings>,
77
84
  is_workspace: RefCell<Option<bool>>,
@@ -100,13 +107,6 @@ pub struct PresentationHandle {
100
107
  /// the duration of the drag.
101
108
  drag_target: RefCell<Option<DragTargetState>>,
102
109
 
103
- /// Per-element dedup cell for `perspective-config-update` event
104
- /// dispatch. Read+written by `crate::custom_events::dispatch_*`
105
- /// helpers; living here means every consumer with a `&Presentation`
106
- /// (subscriptions in `wire_custom_events`, `tasks::send_plugin_config`,
107
- /// `setSelection`) sees the same cache without separate plumbing.
108
- pub last_dispatched_config: RefCell<Option<crate::config::ViewerConfig>>,
109
-
110
110
  pub settings_open_changed: PubSub<bool>,
111
111
 
112
112
  /// Injected callback from the root component, replacing the former
@@ -118,7 +118,7 @@ pub struct PresentationHandle {
118
118
  pub on_eject: PubSub<()>,
119
119
 
120
120
  /// Fires for status-bar / main-panel pointer events that target the
121
- /// statusbar element. `wire_custom_events` formats the `PointerEvent`'s
121
+ /// statusbar element. `wire_element_events` formats the `PointerEvent`'s
122
122
  /// `type_()` into a `perspective-statusbar-{type}` `CustomEvent` name.
123
123
  pub statusbar_pointer_event: PubSub<PointerEvent>,
124
124
  }
@@ -148,7 +148,8 @@ impl Presentation {
148
148
  pub fn new(elem: &HtmlElement) -> Self {
149
149
  let theme = Self(Rc::new(PresentationHandle {
150
150
  viewer_elem: elem.clone(),
151
- theme_data: Default::default(),
151
+ themes: Default::default(),
152
+ theme_init: Default::default(),
152
153
  is_workspace: Default::default(),
153
154
  settings_open_changed: Default::default(),
154
155
  settings_before_open_changed: Default::default(),
@@ -159,7 +160,6 @@ impl Presentation {
159
160
  theme_config_updated: PubSub::default(),
160
161
  on_eject: PubSub::default(),
161
162
  statusbar_pointer_event: PubSub::default(),
162
- last_dispatched_config: Default::default(),
163
163
  drag_state: Default::default(),
164
164
  drop_received: Default::default(),
165
165
  on_dragstart: Default::default(),
@@ -259,14 +259,14 @@ impl Presentation {
259
259
  /// readable stylesheets. This method is memoized - the state can be
260
260
  /// flushed by calling `reset()`.
261
261
  pub async fn get_available_themes(&self) -> ApiResult<PtrEqRc<Vec<String>>> {
262
- let mut data = self.0.theme_data.lock().await;
263
- if data.themes.is_none() {
262
+ let _guard = self.0.theme_init.lock().await;
263
+ if self.0.themes.borrow().is_none() {
264
264
  await_dom_loaded().await?;
265
265
  let themes = sheets::get_theme_names(&self.0.viewer_elem)?;
266
- data.themes = Some(themes);
266
+ *self.0.themes.borrow_mut() = Some(themes);
267
267
  }
268
268
 
269
- Ok(data.themes.clone().unwrap().into())
269
+ Ok(self.0.themes.borrow().clone().unwrap().into())
270
270
  }
271
271
 
272
272
  /// Reset the state. `styleSheets` will be re-parsed next time
@@ -281,9 +281,9 @@ impl Presentation {
281
281
  .unwrap_or_default()
282
282
  }
283
283
 
284
- let mut mutex = self.0.theme_data.lock().await;
285
- let changed = as_set(&mutex.themes) != as_set(&themes);
286
- mutex.themes = themes;
284
+ let _guard = self.0.theme_init.lock().await;
285
+ let changed = as_set(&self.0.themes.borrow()) != as_set(&themes);
286
+ *self.0.themes.borrow_mut() = themes;
287
287
  changed
288
288
  }
289
289
 
@@ -307,6 +307,22 @@ impl Presentation {
307
307
  index.and_then(|x| themes.get(x).cloned())
308
308
  }
309
309
 
310
+ /// The default theme (first registered), used to resolve a panel's
311
+ /// effective theme when it has no per-panel override. `None` if no
312
+ /// themes exist.
313
+ pub async fn get_default_theme_name(&self) -> Option<String> {
314
+ self.get_available_themes().await.ok()?.first().cloned()
315
+ }
316
+
317
+ /// SYNC read of the registry default theme name, derived on demand from
318
+ /// the memoized registry — `None` until the registry first parses (or
319
+ /// when no themes exist). For synchronous stamping paths that must not
320
+ /// await registry init; prefer [`Self::get_default_theme_name`] where
321
+ /// awaiting is acceptable.
322
+ pub fn default_theme_name_sync(&self) -> Option<String> {
323
+ self.0.themes.borrow().as_ref()?.first().cloned()
324
+ }
325
+
310
326
  fn set_theme_attribute(&self, theme: Option<&str>) -> ApiResult<()> {
311
327
  if let Some(theme) = theme {
312
328
  Ok(self.0.viewer_elem.set_attribute("theme", theme)?)
@@ -325,18 +341,35 @@ impl Presentation {
325
341
 
326
342
  /// Set the theme by name, or `None` for the default theme.
327
343
  ///
344
+ /// A NAMED theme's host attribute write is SYNCHRONOUS ("stamp with
345
+ /// commit") — no await separates the caller's config commit from the
346
+ /// attribute the document cascade styles, so a slow theme-registry
347
+ /// init can no longer hold the host on the former theme while e.g. an
348
+ /// initial `restore()`'s first draw resolves. The registry-dependent
349
+ /// tail (theme-list resolution + `theme_config_updated`) follows
350
+ /// asynchronously. `None` (reset to the registry default) still
351
+ /// resolves through the registry first, as the default name is not
352
+ /// knowable synchronously on a cold cache.
353
+ ///
354
+ /// The attribute is stamped even when the requested name is not (yet)
355
+ /// a registered theme — matching the prior behavior for unknown
356
+ /// names, and additionally making an explicitly-requested name that
357
+ /// HAPPENS to be the registry default explicit on the element (the
358
+ /// old resolved-selection no-op left it absent).
359
+ ///
328
360
  /// # Returns
329
361
  /// A `bool` indicating whether the internal state changed.
330
362
  pub async fn set_theme_name(&self, theme: Option<&str>) -> ApiResult<bool> {
331
- let (themes, selected) = self.get_selected_theme_config().await?;
332
- if let Some(x) = selected
333
- && themes.get(x).map(|x| x.as_str()) == theme
334
- {
335
- return Ok(false);
363
+ if let Some(theme) = theme {
364
+ if self.0.viewer_elem.get_attribute("theme").as_deref() == Some(theme) {
365
+ return Ok(false);
366
+ }
367
+
368
+ self.set_theme_attribute(Some(theme))?;
336
369
  }
337
370
 
371
+ let themes = self.get_available_themes().await?;
338
372
  let index = if let Some(theme) = theme {
339
- self.set_theme_attribute(Some(theme))?;
340
373
  themes.iter().position(|x| x == theme)
341
374
  } else if !themes.is_empty() {
342
375
  self.set_theme_attribute(themes.first().map(|x| x.as_str()))?;
@@ -28,27 +28,45 @@ pub async fn get_viewer_config(
28
28
  presentation: &Presentation,
29
29
  ) -> ApiResult<ViewerConfig> {
30
30
  let version = config::API_VERSION.to_string();
31
- let view_config = if let Some(rendered) = session.get_rendered_view_config() {
31
+ let mut view_config = if let Some(rendered) = session.get_rendered_view_config() {
32
32
  (*rendered).clone()
33
33
  } else {
34
34
  session.get_view_config().clone()
35
35
  };
36
+
37
+ // The rendered config may carry transient element-level global filters
38
+ // (master/detail) appended at view-creation; the stored `config.filter` is
39
+ // always the panel's own user filter, so use it here to keep `save`/
40
+ // `getViewConfig` free of global filters.
41
+ view_config.filter = session.get_view_config().filter.clone();
36
42
  let settings = presentation.is_settings_open();
37
43
  let plugin = renderer.metadata().name.clone();
38
44
  let plugin_config = renderer.get_plugin_config();
39
- let theme = presentation.get_selected_theme_name().await;
45
+ // Per-panel theme: a panel's own theme takes precedence; a panel with none
46
+ // records the registry DEFAULT (first registered theme) it actually renders
47
+ // — every panel has a concrete theme and none inherits the active panel's.
48
+ // `None` only when no themes are registered.
49
+ let theme = match renderer.theme() {
50
+ theme @ Some(_) => theme,
51
+ None => presentation.get_selected_theme_name().await,
52
+ };
40
53
  let title = session.get_title();
41
- let table = session.get_table().map(|x| x.get_name().to_owned());
54
+ let table = session
55
+ .get_table()
56
+ .map(|x| x.get_name().to_owned())
57
+ .or_else(|| session.pending_table());
42
58
  let columns_config = renderer.all_columns_configs();
43
59
  Ok(ViewerConfig {
44
- version,
45
- plugin,
46
- title,
47
- plugin_config,
48
- columns_config,
49
60
  settings,
50
- table,
51
- view_config,
52
- theme,
61
+ panel: PanelViewerConfig {
62
+ version,
63
+ plugin,
64
+ title,
65
+ plugin_config,
66
+ columns_config,
67
+ table,
68
+ view_config,
69
+ theme,
70
+ },
53
71
  })
54
72
  }
@@ -37,7 +37,7 @@ pub fn get_plugin_config_schema(
37
37
  renderer: &Renderer,
38
38
  view_config: &ViewConfig,
39
39
  ) -> ApiResult<ColumnConfigSchema> {
40
- let plugin = renderer.get_active_plugin()?;
40
+ let plugin = renderer.ensure_plugin_selected()?;
41
41
  let view_config_js =
42
42
  wasm_bindgen::JsValue::from_serde_ext(view_config).unwrap_or(wasm_bindgen::JsValue::NULL);
43
43
  let raw = plugin._plugin_config_schema(&view_config_js)?;
@@ -60,7 +60,7 @@ pub fn get_column_config_schema(
60
60
  current_value: Option<&serde_json::Map<String, serde_json::Value>>,
61
61
  abs_max: Option<f64>,
62
62
  ) -> ApiResult<ColumnConfigSchema> {
63
- let plugin = renderer.get_active_plugin()?;
63
+ let plugin = renderer.ensure_plugin_selected()?;
64
64
  let plugin_config = renderer.metadata();
65
65
  let names = &plugin_config.config_column_names;
66
66
  let group = view_config
@@ -17,36 +17,62 @@ use wasm_bindgen::JsCast;
17
17
  use web_sys::*;
18
18
 
19
19
  use crate::js::plugin::JsPerspectiveViewerPlugin;
20
+ use crate::utils::RenderGuard;
20
21
 
21
- /// Given an async `task` which draws `plugin`, activates the plugin in stages
22
- /// to prevent screen shearing. First, and plugin is appended ot the DOM with
23
- /// opacity 0, then rendered. After rendering, the previous plugin is removed
24
- /// and the opacity is set back to 1.
22
+ /// Mount `plugin` (once) under its layout slot in the viewer's light DOM.
23
+ /// Idempotent — a plugin that already has a parent is left untouched (only
24
+ /// its `slot` attribute is refreshed). No style read happens here: the
25
+ /// mount may precede the theme stamp, and the plugin's first `draw()`
26
+ /// captures its `--psp-*` CSS fresh by construction ("stamp before draw").
25
27
  ///
26
28
  /// # Arguments
27
29
  /// - `viewer` the root `<perspective-viewer>` element.
28
30
  /// - `plugin` the plugin custom element.
29
- /// - `task` an async task which renders the plugin.
30
- pub async fn activate_plugin<T>(
31
+ /// - `slot` the layout slot name (panel id) to mount the plugin under, or
32
+ /// `None` to use the default unnamed `<slot>` (legacy single-panel behavior).
33
+ pub fn mount_plugin(
31
34
  viewer: &HtmlElement,
32
35
  plugin: &JsPerspectiveViewerPlugin,
33
- task: impl Future<Output = ApiResult<T>>,
34
- ) -> ApiResult<T> {
36
+ slot: Option<&str>,
37
+ ) -> ApiResult<()> {
35
38
  let html_plugin = plugin.unchecked_ref::<HtmlElement>();
39
+ if let Some(slot) = slot {
40
+ html_plugin.set_attribute("slot", slot)?;
41
+ }
42
+
36
43
  if html_plugin.parent_node().is_none() {
37
- html_plugin.style().set_property("opacity", "0")?;
38
- viewer.append_child(html_plugin)?;
39
- plugin.restyle();
44
+ // TODO(texodus): Place at the bottom of the stack so it is rendered
45
+ // but not visible during a tab switch. This is dumb - fix this with
46
+ // a real life cycle.
47
+ viewer.prepend_with_node_1(html_plugin)?;
40
48
  }
41
49
 
42
- let result = task.await;
43
- html_plugin.style().set_property("opacity", "1")?;
44
- result
50
+ Ok(())
45
51
  }
46
52
 
53
+ /// Given an async `task` which draws `plugin`, [`mount_plugin`] it, then run
54
+ /// `task`. Requires the [`RenderGuard`] lock witness — plugin dispatch
55
+ /// outside a locked run does not compile (invariants I3/I6 of the merge;
56
+ /// see `SESSION_CONFIG_COHERENCE_PLAN.md`).
57
+ pub async fn activate_plugin<T>(
58
+ _guard: &RenderGuard,
59
+ viewer: &HtmlElement,
60
+ plugin: &JsPerspectiveViewerPlugin,
61
+ slot: Option<&str>,
62
+ task: impl Future<Output = ApiResult<T>>,
63
+ ) -> ApiResult<T> {
64
+ mount_plugin(viewer, plugin, slot)?;
65
+ task.await
66
+ }
67
+
68
+ /// Remove the previously-active plugin for a panel from the viewer's light DOM.
69
+ ///
70
+ /// When `slot` is `Some`, only a plugin mounted under that same slot (panel) is
71
+ /// removed, so the active plugins of *other* panels are left untouched.
47
72
  pub fn remove_inactive_plugin(
48
73
  viewer: &HtmlElement,
49
74
  plugin: &JsPerspectiveViewerPlugin,
75
+ slot: Option<&str>,
50
76
  plugins: &[JsPerspectiveViewerPlugin],
51
77
  ) -> ApiResult<()> {
52
78
  for idx in 0..viewer.children().length() {
@@ -55,6 +81,7 @@ pub fn remove_inactive_plugin(
55
81
  && plugins
56
82
  .iter()
57
83
  .any(|x| *x.unchecked_ref::<Element>() == elem)
84
+ && (slot.is_none() || elem.get_attribute("slot").as_deref() == slot)
58
85
  {
59
86
  viewer.remove_child(&elem)?;
60
87
  break;