@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
@@ -10,8 +10,12 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ use std::cell::RefCell;
14
+ use std::collections::HashMap;
15
+
13
16
  use perspective_js::JsViewWindow;
14
17
  use perspective_js::utils::*;
18
+ use wasm_bindgen::JsCast;
15
19
  use wasm_bindgen::prelude::*;
16
20
 
17
21
  use crate::config::PluginStaticConfig;
@@ -25,6 +29,93 @@ extern "C" {
25
29
  #[derive(Clone)]
26
30
  pub type JsPerspectiveViewer;
27
31
 
32
+ /// A `<perspective-viewer>` plugin custom element.
33
+ ///
34
+ /// # Capability tiers
35
+ ///
36
+ /// Only `get_static_config`, `draw`, `save`/`restore` and `delete` are
37
+ /// REQUIRED. Every other method is optional — detected once per element
38
+ /// tag ([`PluginCapabilities`], methods live on the class prototype) —
39
+ /// with a defined host fallback, so a minimal plugin renders correctly
40
+ /// and each added method buys performance or polish, never correctness:
41
+ ///
42
+ /// | method | host fallback when absent |
43
+ /// |------------|--------------------------------------------------------|
44
+ /// | `update` | `draw` — every repaint is a full render (such plugins |
45
+ /// | | must NOT treat `draw` as a state-reset license) |
46
+ /// | `resize` | no-op — CSS reflows the box; next dispatch repaints |
47
+ /// | `restyle` | no-op — the plugin re-reads CSS at its next render |
48
+ /// | `clear` | no-op |
49
+ /// | `deselect` | no-op |
50
+ /// | `presize` | held style-override presize (`Renderer` resize paths) |
51
+ /// | `render` | image export reports unsupported |
52
+ ///
53
+ /// # Call discipline
54
+ ///
55
+ /// Plugin implementations assume the host NEVER overlaps calls on the
56
+ /// same element: each of the rendering methods (`draw`, `update`,
57
+ /// `render`, `clear`, `resize`) and `delete` must run to completion
58
+ /// before the next begins. Every call site must therefore hold that
59
+ /// plugin's per-`Renderer` draw lock, witnessed by
60
+ /// [`crate::utils::RenderGuard`] — the guard-taking dispatch helpers
61
+ /// (`Renderer::draw_fresh`/`update_bound`,
62
+ /// `renderer::activate::activate_plugin`) and the already-locked
63
+ /// `render_task`/`update_lazy`/`resize`/`restyle_all`/`dispose`
64
+ /// entrypoints do not compile from an unlocked context. Synchronous
65
+ /// calls (`restore`, `restyle`, the schema queries) are exempt, though
66
+ /// `restyle` should ride inside a locked restyle-then-update sequence
67
+ /// (see `Renderer::restyle_all`).
68
+ ///
69
+ /// # Dispatch semantics (`PLUGIN_DRAW_INVARIANT_PLAN.md`, amended
70
+ /// 2026-07-16)
71
+ ///
72
+ /// The host invokes `draw` **iff there is a NEW `View` for this
73
+ /// plugin** — `bind_view` REBUILT the engine `View`, or a
74
+ /// freshly-selected plugin element owes its first paint of the bound
75
+ /// one — so implementations may treat `draw` as "new data shape"
76
+ /// (reset zoom/scroll/domain state). The witness enforcing this is
77
+ /// [`crate::session::FreshView`], which only the rebuild and
78
+ /// first-paint paths mint.
79
+ ///
80
+ /// `update` is a repaint of the SAME `View`, invoked iff a
81
+ /// plugin-visible SOURCE changed — one of exactly six: (1) data
82
+ /// (`View::on_update`), (2) an adopted placeholder-config delta,
83
+ /// (3) a genuinely CHANGED `plugin_config`/`columns_config` the host
84
+ /// just delivered via `restore` (so `update`-after-`restore` is the
85
+ /// sanctioned delivery pairing), (4) genuinely changed CSS vars just
86
+ /// applied via `restyle` (`Renderer::restyle_all`, or the fused
87
+ /// stale-capture restyle immediately preceding a dispatch inside
88
+ /// `draw_view` — gated by `Renderer::needs_restyle`), (5) a
89
+ /// render-limits change (warning dismissed), (6) an explicit PUBLIC
90
+ /// element-API call that reconciled as a no-op
91
+ /// ([`crate::tasks::RunOrigin::Public`] — the documented
92
+ /// `viewer.restore({})` refresh affordance). An INTERNAL run that
93
+ /// changed none of these dispatches nothing.
94
+ ///
95
+ /// `resize` is geometry/chrome only, from retained state (and is also
96
+ /// the activation-chrome nudge — implementations should no-op it
97
+ /// while hidden).
98
+ ///
99
+ /// `restore`/`save` are state transfer, NOT rendering: plugins must
100
+ /// not render internally from them, and must not call host APIs from
101
+ /// inside `restore` (a `restore`-triggered `restorePanel` echo is how
102
+ /// the initial-load double render happened — user gestures may echo,
103
+ /// host-delivered restores must not). (The built-in `Debug` plugin's
104
+ /// own `resize()` delegating to its `draw()` is plugin-INTERNAL and
105
+ /// exempt.)
106
+ ///
107
+ /// # Render-callable contract (invariant I5,
108
+ /// `SESSION_CONFIG_COHERENCE_PLAN.md`)
109
+ ///
110
+ /// From inside `draw`/`update`/`render`/`resize`, a plugin may call
111
+ /// back into its host `<perspective-viewer>` ONLY the per-panel
112
+ /// snapshot getters — `getViewConfigPanel`, `getTablePanel`,
113
+ /// `getClientPanel`, `getEditPortPanel` — which answer from the run's
114
+ /// pinned `RenderContext`, so plugin-visible state always equals the
115
+ /// snapshot being drawn, never a fresher in-flight commit.
116
+ /// Lock-acquiring host methods (`savePanel`, `restorePanel`, `flush`,
117
+ /// `download`/`export`/`copy`, …) DEADLOCK on the non-reentrant draw
118
+ /// lock if called from a render and are forbidden.
28
119
  #[derive(Clone)]
29
120
  pub type JsPerspectiveViewerPlugin;
30
121
 
@@ -51,24 +142,41 @@ extern "C" {
51
142
  #[wasm_bindgen(method, catch, js_name = plugin_config_schema)]
52
143
  pub fn _plugin_config_schema(this: &JsPerspectiveViewerPlugin, view_config: &JsValue) -> ApiResult<JsValue>;
53
144
 
145
+ /// STATE TRANSFER, not rendering (dispatch semantics, rule for
146
+ /// `restore`/`save`): deliver a `plugin_config` + `columns_config`
147
+ /// snapshot into the plugin. Sync and lock-exempt. The host pairs a
148
+ /// restore that genuinely CHANGED plugin state with exactly one
149
+ /// `update` in the same locked run (update source 3) — plugins must
150
+ /// not render from `restore()` themselves, and must not call host
151
+ /// APIs from inside it (the echo rule: a plugin-issued `restorePanel`
152
+ /// re-enters through the PUBLIC surface, indistinguishable from a
153
+ /// user call — the initial-load double-render bug). Prefer the typed
154
+ /// [`JsPerspectiveViewerPlugin::restore`] wrapper.
54
155
  #[wasm_bindgen(method, js_name=restore, catch)]
55
156
  pub fn _restore(this: &JsPerspectiveViewerPlugin, token: &JsValue, columns_config: &JsValue) -> ApiResult<()>;
56
157
 
158
+ /// Free the plugin's resources. Serialized like the rendering methods —
159
+ /// callers route through the draw lock (`Renderer::dispose`/`delete`
160
+ /// defer teardown through it; a sync `delete` mid-`draw` violates the
161
+ /// call discipline).
57
162
  #[wasm_bindgen(method)]
58
163
  pub fn delete(this: &JsPerspectiveViewerPlugin);
59
164
 
60
- #[wasm_bindgen(method)]
61
- pub fn restyle(
165
+ #[wasm_bindgen(method, js_name = restyle)]
166
+ fn _restyle(
62
167
  this: &JsPerspectiveViewerPlugin,
63
168
  );
64
169
 
65
- #[wasm_bindgen(method, catch)]
66
- pub async fn render(
170
+ #[wasm_bindgen(method, catch, js_name = render)]
171
+ async fn _render(
67
172
  this: &JsPerspectiveViewerPlugin,
68
173
  view: perspective_js::View,
69
174
  viewport: Option<JsViewWindow>,
70
175
  ) -> ApiResult<web_sys::Blob>;
71
176
 
177
+ /// Full render of a `View` that is NEW to this plugin — dispatched iff
178
+ /// `bind_view` REBUILT the engine `View`. The only REQUIRED rendering
179
+ /// method.
72
180
  #[wasm_bindgen(method, catch)]
73
181
  pub async fn draw(
74
182
  this: &JsPerspectiveViewerPlugin,
@@ -78,8 +186,8 @@ extern "C" {
78
186
  force: bool
79
187
  ) -> ApiResult<()>;
80
188
 
81
- #[wasm_bindgen(method, catch)]
82
- pub async fn update(
189
+ #[wasm_bindgen(method, catch, js_name = update)]
190
+ async fn _update(
83
191
  this: &JsPerspectiveViewerPlugin,
84
192
  view: perspective_js::View,
85
193
  column_limit: Option<usize>,
@@ -87,12 +195,17 @@ extern "C" {
87
195
  force: bool
88
196
  ) -> ApiResult<()>;
89
197
 
90
- #[wasm_bindgen(method, catch)]
91
- pub async fn clear(this: &JsPerspectiveViewerPlugin) -> ApiResult<JsValue>;
198
+ #[wasm_bindgen(method, catch, js_name = clear)]
199
+ async fn _clear(this: &JsPerspectiveViewerPlugin) -> ApiResult<JsValue>;
92
200
 
93
- #[wasm_bindgen(method, catch)]
94
- pub async fn resize(this: &JsPerspectiveViewerPlugin) -> ApiResult<JsValue>;
201
+ #[wasm_bindgen(method, catch, js_name = resize)]
202
+ async fn _resize(this: &JsPerspectiveViewerPlugin) -> ApiResult<JsValue>;
203
+
204
+ #[wasm_bindgen(method, catch, js_name = deselect)]
205
+ async fn _deselect(this: &JsPerspectiveViewerPlugin) -> ApiResult<()>;
95
206
 
207
+ #[wasm_bindgen(method, catch, js_name = presize)]
208
+ async fn _presize(this: &JsPerspectiveViewerPlugin, width: f64, height: f64) -> ApiResult<JsValue>;
96
209
  }
97
210
 
98
211
  impl From<JsPluginStaticConfig> for PluginStaticConfig {
@@ -101,6 +214,49 @@ impl From<JsPluginStaticConfig> for PluginStaticConfig {
101
214
  }
102
215
  }
103
216
 
217
+ /// Which OPTIONAL plugin methods a plugin implements (see "Capability
218
+ /// tiers" on [`JsPerspectiveViewerPlugin`]). Detected once per
219
+ /// custom-element tag — methods live on the class prototype, so every
220
+ /// instance of a tag answers identically — and memoized; call sites read
221
+ /// cached flags instead of running per-call `Reflect` probes.
222
+ #[derive(Clone, Copy, Debug, Default)]
223
+ pub struct PluginCapabilities {
224
+ pub draw: bool,
225
+ pub update: bool,
226
+ pub resize: bool,
227
+ pub restyle: bool,
228
+ pub clear: bool,
229
+ pub deselect: bool,
230
+ pub presize: bool,
231
+ pub render: bool,
232
+ }
233
+
234
+ thread_local! {
235
+ static PLUGIN_CAPABILITIES: RefCell<HashMap<String, PluginCapabilities>> =
236
+ RefCell::new(HashMap::new());
237
+ }
238
+
239
+ impl PluginCapabilities {
240
+ fn detect(plugin: &JsPerspectiveViewerPlugin) -> Self {
241
+ let is_fn = |name: &str| {
242
+ js_sys::Reflect::get(plugin, &JsValue::from_str(name))
243
+ .map(|x| x.is_function())
244
+ .unwrap_or_default()
245
+ };
246
+
247
+ Self {
248
+ draw: is_fn("draw"),
249
+ update: is_fn("update"),
250
+ resize: is_fn("resize"),
251
+ restyle: is_fn("restyle"),
252
+ clear: is_fn("clear"),
253
+ deselect: is_fn("deselect"),
254
+ presize: is_fn("presize"),
255
+ render: is_fn("render"),
256
+ }
257
+ }
258
+ }
259
+
104
260
  impl JsPerspectiveViewerPlugin {
105
261
  /// Read and deserialize the plugin's static config. Should only
106
262
  /// be called once per plugin (at registration time); cache the
@@ -118,4 +274,135 @@ impl JsPerspectiveViewerPlugin {
118
274
  let columns_config = JsValue::from_serde_ext(&columns_config).unwrap();
119
275
  self._restore(token, &columns_config)
120
276
  }
277
+
278
+ /// This plugin's memoized [`PluginCapabilities`] — the first call for a
279
+ /// given element tag runs the `Reflect` probes; all later calls (any
280
+ /// instance of the tag) read the cache.
281
+ pub fn capabilities(&self) -> PluginCapabilities {
282
+ let tag = self.unchecked_ref::<web_sys::Element>().tag_name();
283
+ PLUGIN_CAPABILITIES.with(|cache| {
284
+ *cache
285
+ .borrow_mut()
286
+ .entry(tag)
287
+ .or_insert_with(|| PluginCapabilities::detect(self))
288
+ })
289
+ }
290
+
291
+ /// Repaint of the SAME `View` — dispatched iff one of the six
292
+ /// plugin-visible sources changed (see "Dispatch semantics" on
293
+ /// [`JsPerspectiveViewerPlugin`]); never defensively. Reaches the
294
+ /// plugin via [`crate::renderer::Renderer::update_bound`] (pipeline
295
+ /// runs — `Adopted` deltas, changed-config delivery, public no-op
296
+ /// refresh) or [`crate::renderer::Renderer::update_lazy`]
297
+ /// (`table_updated` data refreshes, the config-apply tasks, warning
298
+ /// dismiss), both locked. Tier fallback: a plugin without `update`
299
+ /// receives `draw` — it declared no incremental path, so every repaint
300
+ /// is a full render.
301
+ pub async fn update(
302
+ &self,
303
+ view: perspective_js::View,
304
+ column_limit: Option<usize>,
305
+ row_limit: Option<usize>,
306
+ force: bool,
307
+ ) -> ApiResult<()> {
308
+ if self.capabilities().update {
309
+ self._update(view, column_limit, row_limit, force).await
310
+ } else {
311
+ self.draw(view, column_limit, row_limit, force).await
312
+ }
313
+ }
314
+
315
+ /// Repaint from retained state — dispatched iff geometry or visibility
316
+ /// changed: box resizes (resize observers, presize sweeps, settings
317
+ /// toggles) and the panel-ACTIVATION chrome nudge
318
+ /// ([`crate::renderer::Renderer::activation_repaint`], stamped inside
319
+ /// one locked dispatch). Same `View`, same data, no CSS re-read —
320
+ /// implementations should no-op while hidden (`offsetParent == null`).
321
+ /// Tier fallback: no-op — CSS reflows the box and the next dispatch
322
+ /// repaints the content.
323
+ pub async fn resize(&self) -> ApiResult<JsValue> {
324
+ if self.capabilities().resize {
325
+ self._resize().await
326
+ } else {
327
+ Ok(JsValue::UNDEFINED)
328
+ }
329
+ }
330
+
331
+ /// Re-read the `--psp-*` CSS custom properties (sync). Dispatched ONLY
332
+ /// when the effective theme genuinely changed, state-keyed by
333
+ /// [`crate::renderer::Renderer::needs_restyle`] (the effective theme
334
+ /// vs. the one recorded at this plugin's last capture — first paint or
335
+ /// last restyle), from two sites: fused immediately BEFORE a
336
+ /// `draw`/`update` inside the same locked dispatch
337
+ /// (`Renderer::draw_view`, "restyle then draw" — one render pass in
338
+ /// the new theme), or as `Renderer::restyle_all`'s locked
339
+ /// restyle-then-`update` pair when no other dispatch is coming
340
+ /// (theme picker, `resetThemes`, default-theme discovery, restore
341
+ /// tails). Exception: the public `restyleElement()` API restyles
342
+ /// unconditionally — it is the "my external CSS changed" affordance,
343
+ /// outside what captured-theme state can know. (Plus one
344
+ /// `mount_plugin` restyle on first light-DOM mount.) Tier fallback:
345
+ /// no-op — the plugin re-reads CSS at its next render.
346
+ pub fn restyle(&self) {
347
+ if self.capabilities().restyle {
348
+ self._restyle()
349
+ }
350
+ }
351
+
352
+ /// Blank the plugin (view deleted, table removed). Tier fallback:
353
+ /// no-op.
354
+ pub async fn clear(&self) -> ApiResult<JsValue> {
355
+ if self.capabilities().clear {
356
+ self._clear().await
357
+ } else {
358
+ Ok(JsValue::UNDEFINED)
359
+ }
360
+ }
361
+
362
+ /// Offscreen export render (`copy`/`export`) — returns an image
363
+ /// `Blob`; not a dispatch verb (does not touch the plugin's mounted
364
+ /// DOM state), but still serialized under the draw lock. Tier
365
+ /// fallback: errors — image export is unsupported without it.
366
+ pub async fn render(
367
+ &self,
368
+ view: perspective_js::View,
369
+ viewport: Option<JsViewWindow>,
370
+ ) -> ApiResult<web_sys::Blob> {
371
+ if self.capabilities().render {
372
+ self._render(view, viewport).await
373
+ } else {
374
+ Err(ApiError::from("Plugin does not support image export"))
375
+ }
376
+ }
377
+
378
+ /// Clear any visible selection state (highlighted rows, pinned
379
+ /// tooltips) WITHOUT emitting selection events. Invoked by the host
380
+ /// when an element-level global filter contributed by this panel's
381
+ /// selection is removed (`GlobalFilterBar` chip × / "Clear"), so the
382
+ /// selection visual can't outlive the filter it produced.
383
+ /// Implementations may redraw, so callers must hold the plugin's
384
+ /// per-`Renderer` draw lock (see "Call discipline"). Tier fallback:
385
+ /// no-op.
386
+ pub async fn deselect(&self) -> ApiResult<()> {
387
+ if self.capabilities().deselect {
388
+ self._deselect().await
389
+ } else {
390
+ Ok(())
391
+ }
392
+ }
393
+
394
+ /// Staged presize: render at the TARGET element box `(width, height)`
395
+ /// offscreen — nothing on screen changes — resolving, once the frame
396
+ /// is staged, to a present closure that reveals it synchronously.
397
+ /// Callers run the closure in the same task as the layout commit the
398
+ /// presize anticipated (see `Renderer::presize_with_dimensions`), so
399
+ /// geometry and pixels land in one paint; the closure is lock-free (it
400
+ /// blits retained pixels, no plugin render entry). Only call when
401
+ /// [`Self::capabilities`] reports `presize` — plugins without it take
402
+ /// the held style-override path. Resolves `None` when the plugin
403
+ /// skipped (hidden), presenting nothing.
404
+ pub async fn presize(&self, width: f64, height: f64) -> ApiResult<Option<js_sys::Function>> {
405
+ let present = self._presize(width, height).await?;
406
+ Ok(present.dyn_into::<js_sys::Function>().ok())
407
+ }
121
408
  }
@@ -0,0 +1,258 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ //! A `wasm-bindgen` binding to the `<regular-layout>` custom element from
14
+ //! [`regular-layout`](https://github.com/texodus/regular-layout), used as the
15
+ //! layout engine for `<perspective-viewer>`'s panels.
16
+
17
+ use js_sys::Promise;
18
+ use serde::{Deserialize, Serialize};
19
+ use wasm_bindgen::prelude::*;
20
+
21
+ #[wasm_bindgen]
22
+ extern "C" {
23
+ /// The `<regular-layout>` custom element.
24
+ #[wasm_bindgen(extends = web_sys::HtmlElement)]
25
+ pub type RegularLayout;
26
+
27
+ /// Place the child named `name` into the grid, optionally at `path`
28
+ /// (a `number[]`; pass [`JsValue::UNDEFINED`] to append) and optionally
29
+ /// forcing a `split` (`bool` or an `Orientation` string).
30
+ #[wasm_bindgen(method, js_name = "insertPanel")]
31
+ pub fn insert_panel(this: &RegularLayout, name: &str, path: JsValue, split: JsValue)
32
+ -> Promise;
33
+
34
+ /// Remove the panel named `name` from the grid (its DOM child stays
35
+ /// connected, but unslotted).
36
+ #[wasm_bindgen(method, js_name = "removePanel")]
37
+ pub fn remove_panel(this: &RegularLayout, name: &str) -> Promise;
38
+
39
+ /// Unslot all panels.
40
+ #[wasm_bindgen(method)]
41
+ pub fn clear(this: &RegularLayout) -> Promise;
42
+
43
+ /// Return the `number[]` index path for the panel named `name`, or `null`.
44
+ #[wasm_bindgen(method, js_name = "calculatePath")]
45
+ pub fn calculate_path(this: &RegularLayout, name: &str) -> JsValue;
46
+
47
+ /// Hit-test for the [`LayoutPath`](https://github.com/texodus/regular-layout)
48
+ /// under a pointer (`{clientX, clientY}`), or `null`.
49
+ #[wasm_bindgen(method, js_name = "calculateIntersect")]
50
+ pub fn calculate_intersect(this: &RegularLayout, coordinates: &JsValue) -> JsValue;
51
+
52
+ /// Show the drag-preview overlay for `target` at the pointer `coordinates`.
53
+ #[wasm_bindgen(method, js_name = "setOverlayState")]
54
+ pub fn set_overlay_state(
55
+ this: &RegularLayout,
56
+ coordinates: &JsValue,
57
+ target: &JsValue,
58
+ ) -> Promise;
59
+
60
+ /// Clear the drag-preview overlay and commit the placement for `target`.
61
+ #[wasm_bindgen(method, js_name = "clearOverlayState")]
62
+ pub fn clear_overlay_state(
63
+ this: &RegularLayout,
64
+ coordinates: &JsValue,
65
+ target: &JsValue,
66
+ ) -> Promise;
67
+
68
+ /// Euclidean pixel distance between `coordinates` and a drag `target`.
69
+ #[wasm_bindgen(method, js_name = "diffCoordinates")]
70
+ pub fn diff_coordinates(this: &RegularLayout, coordinates: &JsValue, target: &JsValue) -> f64;
71
+
72
+ /// Convert a `ViewWindow` (normalized 0–1) to a `DOMRect` in screen pixels.
73
+ #[wasm_bindgen(method, js_name = "realCoordinates")]
74
+ pub fn real_coordinates(this: &RegularLayout, window: &JsValue) -> JsValue;
75
+
76
+ /// Configure resize physics (edge tolerances, divider size).
77
+ #[wasm_bindgen(method, js_name = "restorePhysics")]
78
+ pub fn restore_physics(this: &RegularLayout, physics: &JsValue);
79
+
80
+ /// Read the current resize physics.
81
+ #[wasm_bindgen(method, js_name = "savePhysics")]
82
+ pub fn save_physics(this: &RegularLayout) -> JsValue;
83
+
84
+ /// Resume a resize previously deferred via `preventDefault()` in the
85
+ /// `regular-layout-before-resize` handler.
86
+ #[wasm_bindgen(method, js_name = "resumeResize")]
87
+ pub fn resume_resize(this: &RegularLayout);
88
+
89
+ /// Serialize the current [`Layout`] tree.
90
+ #[wasm_bindgen(method)]
91
+ pub fn save(this: &RegularLayout) -> JsValue;
92
+
93
+ /// Restore a [`Layout`] tree, dispatching a cancelable
94
+ /// `regular-layout-before-resize` event first.
95
+ #[wasm_bindgen(method)]
96
+ pub fn restore(this: &RegularLayout, layout: &JsValue) -> Promise;
97
+
98
+ /// Restore a [`Layout`] tree synchronously, without the resize event.
99
+ #[wasm_bindgen(method, js_name = "restoreSync")]
100
+ pub fn restore_sync(this: &RegularLayout, layout: &JsValue);
101
+
102
+ /// Select the panel `name`, making it the front-most tab within its
103
+ /// containing stack; dispatches a `regular-layout-select` event
104
+ /// (`detail: { name }`). Fires even when re-selecting the active tab.
105
+ #[wasm_bindgen(method)]
106
+ pub fn select(this: &RegularLayout, name: &str) -> Promise;
107
+
108
+ /// Maximize the panel `name` (full-size, others hidden). Transient — not
109
+ /// persisted by [`Self::save`]; a subsequent [`Self::restore`] resets it.
110
+ #[wasm_bindgen(method)]
111
+ pub fn maximize(this: &RegularLayout, name: &str);
112
+
113
+ /// Restore the normal multi-panel view after [`Self::maximize`].
114
+ #[wasm_bindgen(method)]
115
+ pub fn minimize(this: &RegularLayout);
116
+
117
+ /// The `detail` of a [`Self::BEFORE_RESIZE_EVENT`] event.
118
+ pub type PresizeDetail;
119
+
120
+ /// Each panel's pending-resize target as a `Record<name, LayoutPath>`; each
121
+ /// `LayoutPath` carries a normalized `view_window` that
122
+ /// [`Self::real_coordinates`] converts to a pixel box. Used to pre-size
123
+ /// panel contents before the layout commits.
124
+ #[wasm_bindgen(method, js_name = "calculatePresizePaths")]
125
+ pub fn calculate_presize_paths(this: &PresizeDetail) -> JsValue;
126
+
127
+ /// For a drag transition, the dragged panel's `{slot, path}` — its name
128
+ /// and the `LayoutPath` of its preview box (`path.view_window` is exactly
129
+ /// where the overlay renders it). The dragged panel is removed from the
130
+ /// layout tree during a drag, so it is absent from
131
+ /// [`Self::calculate_presize_paths`]; this getter lets consumers pre-size
132
+ /// it like any other panel (regular-layout >= 0.6.1). `null` when the
133
+ /// pointer is outside every drop target (the dragged panel is hidden),
134
+ /// `undefined` for non-drag transitions.
135
+ #[wasm_bindgen(method, getter)]
136
+ pub fn overlay(this: &PresizeDetail) -> JsValue;
137
+ }
138
+
139
+ impl RegularLayout {
140
+ /// Emitted before the layout resizes its panels, so consumers can pre-size
141
+ /// panel contents (`CustomEvent<PresizeDetail>`); cancelable.
142
+ pub const BEFORE_RESIZE_EVENT: &'static str = "regular-layout-before-resize";
143
+ /// Emitted before the layout tree changes (`CustomEvent<Layout>`).
144
+ pub const BEFORE_UPDATE_EVENT: &'static str = "regular-layout-before-update";
145
+ /// The `<regular-layout-frame>` chrome element tag name (titlebar + tabs).
146
+ pub const FRAME_TAG_NAME: &'static str = "regular-layout-frame";
147
+ /// Emitted when a tab is selected (`CustomEvent<{ name: string }>`). Fires
148
+ /// on every tab interaction — including re-selecting the active tab or
149
+ /// selecting the sole tab of a single-element stack — so consumers can
150
+ /// always map a tab interaction to an "active panel".
151
+ pub const SELECT_EVENT: &'static str = "regular-layout-select";
152
+ /// The custom element tag name.
153
+ pub const TAG_NAME: &'static str = "regular-layout";
154
+ /// Emitted after the layout tree changes (`CustomEvent<Layout>`).
155
+ pub const UPDATE_EVENT: &'static str = "regular-layout-update";
156
+
157
+ /// Whether `name` is currently placed in this layout's tree (a non-null
158
+ /// [`Self::calculate_path`]).
159
+ pub fn contains_panel(&self, name: &str) -> bool {
160
+ let path = self.calculate_path(name);
161
+ !path.is_null() && !path.is_undefined()
162
+ }
163
+ }
164
+
165
+ /// The orientation of a [`SplitLayout`].
166
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
167
+ #[serde(rename_all = "lowercase")]
168
+ pub enum Orientation {
169
+ Horizontal,
170
+ Vertical,
171
+ }
172
+
173
+ /// A serde mirror of `regular-layout`'s serialized layout tree, as returned by
174
+ /// [`RegularLayout::save`] and accepted by [`RegularLayout::restore`].
175
+ #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ts_rs::TS)]
176
+ #[serde(tag = "type")]
177
+ pub enum Layout {
178
+ /// Space divided among children, side-by-side or stacked.
179
+ #[serde(rename = "split-layout")]
180
+ Split(SplitLayout),
181
+
182
+ /// A leaf node holding one or more named tabs (a stack).
183
+ #[serde(rename = "tab-layout")]
184
+ Tab(TabLayout),
185
+ }
186
+
187
+ #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ts_rs::TS)]
188
+ pub struct SplitLayout {
189
+ pub children: Vec<Layout>,
190
+ pub sizes: Vec<f64>,
191
+ pub orientation: Orientation,
192
+ }
193
+
194
+ #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ts_rs::TS)]
195
+ pub struct TabLayout {
196
+ /// Panel ids (slot names) in this stack.
197
+ pub tabs: Vec<String>,
198
+
199
+ /// Index of the selected tab within `tabs`.
200
+ #[serde(skip_serializing_if = "Option::is_none")]
201
+ #[ts(optional)]
202
+ pub selected: Option<usize>,
203
+ }
204
+
205
+ impl Layout {
206
+ /// Collect every panel id (slot name) in the tree, depth-first.
207
+ pub fn slot_names(&self) -> Vec<String> {
208
+ match self {
209
+ Layout::Split(split) => split.children.iter().flat_map(Layout::slot_names).collect(),
210
+ Layout::Tab(tab) => tab.tabs.clone(),
211
+ }
212
+ }
213
+
214
+ /// Collect every panel id that is *hidden* — i.e. in a stack (tab-layout)
215
+ /// at an index other than its `selected` one — in depth-first order. The
216
+ /// complement of the visible set: every other panel is the front of its
217
+ /// stack (single-tab leaves are trivially visible).
218
+ pub fn hidden_slot_names(&self) -> Vec<String> {
219
+ match self {
220
+ Layout::Split(split) => split
221
+ .children
222
+ .iter()
223
+ .flat_map(Layout::hidden_slot_names)
224
+ .collect(),
225
+ Layout::Tab(tab) => {
226
+ let selected = tab.selected.unwrap_or(0);
227
+ tab.tabs
228
+ .iter()
229
+ .enumerate()
230
+ .filter(|(idx, _)| *idx != selected)
231
+ .map(|(_, name)| name.clone())
232
+ .collect()
233
+ },
234
+ }
235
+ }
236
+
237
+ /// Rewrite every panel id through `f`, returning the remapped tree. Ids for
238
+ /// which `f` returns `None` are left unchanged. Used by whole-element
239
+ /// `restore`, which recreates panels under fresh (collision-free) ids and
240
+ /// must point the saved layout tree at them.
241
+ pub fn remap(&self, f: &impl Fn(&str) -> Option<String>) -> Layout {
242
+ match self {
243
+ Layout::Split(split) => Layout::Split(SplitLayout {
244
+ children: split.children.iter().map(|c| c.remap(f)).collect(),
245
+ sizes: split.sizes.clone(),
246
+ orientation: split.orientation,
247
+ }),
248
+ Layout::Tab(tab) => Layout::Tab(TabLayout {
249
+ tabs: tab
250
+ .tabs
251
+ .iter()
252
+ .map(|name| f(name).unwrap_or_else(|| name.clone()))
253
+ .collect(),
254
+ selected: tab.selected,
255
+ }),
256
+ }
257
+ }
258
+ }
package/src/rust/lib.rs CHANGED
@@ -23,10 +23,13 @@
23
23
  #![warn(
24
24
  clippy::all,
25
25
  clippy::panic_in_result_fn,
26
- clippy::await_holding_refcell_ref,
27
26
  clippy::fallible_impl_from,
28
27
  clippy::unneeded_field_pattern
29
28
  )]
29
+ // Holding a `Ref` across an `await` is the sibling footgun of the config
30
+ // write-back race this crate was rearchitected against (see
31
+ // `SESSION_CONFIG_COHERENCE_PLAN.md`) — deny, don't warn.
32
+ #![deny(clippy::await_holding_refcell_ref)]
30
33
 
31
34
  pub mod components;
32
35
  pub mod config;
@@ -45,6 +48,7 @@ mod session;
45
48
  #[doc(hidden)]
46
49
  pub mod tasks;
47
50
  pub mod utils;
51
+ mod workspace;
48
52
 
49
53
  #[macro_use]
50
54
  extern crate macro_rules_attribute;
@@ -80,8 +84,23 @@ import type {
80
84
  export type * from "../../src/ts/ts-rs/ViewerConfig.d.ts";
81
85
  export type * from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts";
82
86
  export type * from "../../src/ts/ts-rs/PluginStaticConfig.d.ts";
87
+ export type * from "../../src/ts/ts-rs/WorkspaceConfig.d.ts";
88
+ export type * from "../../src/ts/ts-rs/WorkspaceConfigUpdate.d.ts";
89
+ export type * from "../../src/ts/ts-rs/ExportMethod.d.ts";
90
+ export type * from "../../src/ts/ts-rs/PanelOptions.d.ts";
91
+ export type * from "../../src/ts/ts-rs/ClientOptions.d.ts";
92
+ export type * from "../../src/ts/ts-rs/ExportOptions.d.ts";
93
+ export type * from "../../src/ts/ts-rs/GetTableOptions.d.ts";
94
+ export type * from "../../src/ts/ts-rs/GetClientOptions.d.ts";
95
+ import type {GetTableOptions} from "../../src/ts/ts-rs/GetTableOptions.d.ts";
96
+ import type {PanelOptions} from "../../src/ts/ts-rs/PanelOptions.d.ts";
97
+ import type {ExportOptions} from "../../src/ts/ts-rs/ExportOptions.d.ts";
98
+ import type {GetClientOptions} from "../../src/ts/ts-rs/GetClientOptions.d.ts";
99
+ import type {ClientOptions} from "../../src/ts/ts-rs/ClientOptions.d.ts";
83
100
  import type {ViewerConfig} from "../../src/ts/ts-rs/ViewerConfig.d.ts";
84
101
  import type {ViewerConfigUpdate} from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts";
102
+ import type {WorkspaceConfig} from "../../src/ts/ts-rs/WorkspaceConfig.d.ts";
103
+ import type {WorkspaceConfigUpdate} from "../../src/ts/ts-rs/WorkspaceConfigUpdate.d.ts";
85
104
  "#;
86
105
 
87
106
  /// Register a plugin globally.