@perspective-dev/viewer 5.1.0 → 5.3.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 (245) hide show
  1. package/dist/cdn/perspective-viewer.js +9 -9
  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/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/docs/perspective-docs.json +1 -0
  24. package/dist/esm/column-format.d.ts +19 -28
  25. package/dist/esm/perspective-viewer.d.ts +3 -0
  26. package/dist/esm/perspective-viewer.inline.js +9 -9
  27. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  28. package/dist/esm/perspective-viewer.js +9 -9
  29. package/dist/esm/perspective-viewer.js.map +4 -4
  30. package/dist/esm/plugin.d.ts +10 -0
  31. package/dist/esm/providers.d.ts +79 -0
  32. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  33. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  34. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  35. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  36. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  37. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  38. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  39. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  40. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  41. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  42. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  43. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  44. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  45. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  46. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +42 -2
  47. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  48. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  49. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  50. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  51. package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
  52. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  53. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  54. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  55. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  56. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  57. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  58. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  59. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  60. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  61. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  62. package/dist/esm/ts-rs/ViewerConfig.d.ts +42 -2
  63. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  64. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +63 -0
  65. package/dist/esm/ts-rs/WindowSpec.d.ts +3 -2
  66. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  67. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +20 -9
  68. package/dist/wasm/perspective-viewer.d.ts +157 -41
  69. package/dist/wasm/perspective-viewer.js +452 -151
  70. package/dist/wasm/perspective-viewer.wasm +0 -0
  71. package/dist/wasm/perspective-viewer.wasm.d.ts +22 -17
  72. package/package.json +2 -1
  73. package/src/css/_viewer_bundle.css +1 -0
  74. package/src/css/column-selector.css +8 -0
  75. package/src/css/column-settings-panel.css +9 -5
  76. package/src/css/column-style.css +209 -31
  77. package/src/css/config-selector.css +38 -3
  78. package/src/css/dom/select.css +2 -1
  79. package/src/css/form/code-editor.css +6 -7
  80. package/src/css/form/mirrored-textarea.css +81 -0
  81. package/src/css/viewer.css +328 -0
  82. package/src/rust/agent/client/mod.rs +19 -0
  83. package/src/rust/agent/client/protocol.rs +320 -0
  84. package/src/rust/agent/client/transport.rs +287 -0
  85. package/src/rust/agent/client/turn.rs +174 -0
  86. package/src/rust/agent/config.rs +159 -0
  87. package/src/rust/agent/docs.rs +293 -0
  88. package/src/rust/agent/markdown/block.rs +509 -0
  89. package/src/rust/agent/markdown/entity.rs +52 -0
  90. package/src/rust/agent/markdown/events.rs +71 -0
  91. package/src/rust/agent/markdown/inline.rs +497 -0
  92. package/src/rust/agent/markdown/mod.rs +466 -0
  93. package/src/rust/agent/mod.rs +23 -0
  94. package/src/rust/agent/preamble.md +16 -0
  95. package/src/rust/agent/runtime.rs +201 -0
  96. package/src/rust/agent/slot.rs +236 -0
  97. package/src/rust/agent/tools.rs +1141 -0
  98. package/src/rust/components/chat_panel.rs +289 -0
  99. package/src/rust/components/column_selector/active_column.rs +2 -2
  100. package/src/rust/components/column_selector/add_expression_button.rs +3 -3
  101. package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
  102. package/src/rust/components/column_selector/config_selector.rs +84 -7
  103. package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
  104. package/src/rust/components/column_selector/inactive_column.rs +2 -2
  105. package/src/rust/components/column_selector.rs +55 -4
  106. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
  107. package/src/rust/components/column_settings_sidebar/style_tab.rs +262 -225
  108. package/src/rust/components/column_settings_sidebar.rs +88 -69
  109. package/src/rust/components/containers/dragdrop_list.rs +37 -0
  110. package/src/rust/components/editable_header.rs +85 -144
  111. package/src/rust/components/expression_editor.rs +9 -16
  112. package/src/rust/components/form/code_editor.rs +29 -28
  113. package/src/rust/components/form/debug.rs +9 -5
  114. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  115. package/src/rust/components/form/mod.rs +4 -1
  116. package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
  117. package/src/rust/components/form/named_value_picker.rs +86 -0
  118. package/src/rust/components/form/palette_selector.rs +440 -0
  119. package/src/rust/components/main_panel/msg.rs +2 -2
  120. package/src/rust/components/main_panel/reconcile.rs +4 -4
  121. package/src/rust/components/mod.rs +2 -0
  122. package/src/rust/components/plugin_tab.rs +4 -28
  123. package/src/rust/components/settings_panel.rs +105 -22
  124. package/src/rust/components/viewer/msg.rs +46 -7
  125. package/src/rust/components/viewer/panels.rs +17 -20
  126. package/src/rust/components/viewer/render.rs +10 -3
  127. package/src/rust/components/viewer/settings.rs +163 -23
  128. package/src/rust/components/viewer/snapshots.rs +46 -21
  129. package/src/rust/components/viewer/wiring.rs +14 -25
  130. package/src/rust/components/viewer.rs +12 -8
  131. package/src/rust/components/window_editor.rs +75 -103
  132. package/src/rust/config/column_config_schema.rs +100 -41
  133. package/src/rust/config/css_scale.rs +882 -0
  134. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  135. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  136. package/src/rust/config/mod.rs +2 -0
  137. package/src/rust/config/number_string_format.rs +33 -0
  138. package/src/rust/config/options.rs +28 -0
  139. package/src/rust/config/plugin_static_config.rs +120 -5
  140. package/src/rust/config/viewer_config.rs +216 -3
  141. package/src/rust/config/workspace_config.rs +138 -10
  142. package/src/rust/custom_elements/viewer.rs +369 -119
  143. package/src/rust/custom_events.rs +43 -1
  144. package/src/rust/js/regular_layout.rs +3 -3
  145. package/src/rust/lib.rs +15 -0
  146. package/src/rust/presentation/column_locator.rs +19 -5
  147. package/src/rust/presentation.rs +200 -36
  148. package/src/rust/queries/column_locator.rs +74 -44
  149. package/src/rust/queries/get_viewer_config.rs +2 -1
  150. package/src/rust/queries/mod.rs +2 -0
  151. package/src/rust/queries/palette_set.rs +82 -0
  152. package/src/rust/queries/plugin_column_styles.rs +6 -2
  153. package/src/rust/renderer/dispatch.rs +16 -4
  154. package/src/rust/renderer/plugin_config.rs +309 -47
  155. package/src/rust/renderer.rs +15 -42
  156. package/src/rust/session/column_defaults_update.rs +32 -7
  157. package/src/rust/session/metadata.rs +23 -21
  158. package/src/rust/session/props.rs +1 -0
  159. package/src/rust/session/replace_expression_update.rs +1 -0
  160. package/src/rust/session.rs +9 -6
  161. package/src/rust/tasks/create_panel.rs +46 -49
  162. package/src/rust/tasks/edit_expression.rs +19 -23
  163. package/src/rust/tasks/edit_window.rs +9 -11
  164. package/src/rust/tasks/eject.rs +4 -1
  165. package/src/rust/tasks/pipeline.rs +2 -2
  166. package/src/rust/tasks/presize_panels.rs +44 -5
  167. package/src/rust/tasks/reset_all.rs +4 -4
  168. package/src/rust/tasks/restore_and_render.rs +17 -44
  169. package/src/rust/tasks/restore_panel.rs +37 -22
  170. package/src/rust/tasks/set_edit_mode.rs +3 -1
  171. package/src/rust/tasks/sync_update_panels.rs +18 -24
  172. package/src/rust/tasks/update_theme.rs +23 -18
  173. package/src/rust/utils/css_vars.rs +26 -0
  174. package/src/rust/utils/mod.rs +2 -0
  175. package/src/rust/utils/spawn.rs +1 -1
  176. package/src/rust/workspace.rs +454 -199
  177. package/src/svg/lock.svg +7 -0
  178. package/src/themes/botanical.css +10 -0
  179. package/src/themes/defaults.css +12 -0
  180. package/src/themes/dracula.css +17 -0
  181. package/src/themes/gruvbox-dark.css +20 -0
  182. package/src/themes/gruvbox.css +10 -0
  183. package/src/themes/icons.css +1 -0
  184. package/src/themes/intl/de.css +16 -0
  185. package/src/themes/intl/es.css +16 -0
  186. package/src/themes/intl/fr.css +16 -0
  187. package/src/themes/intl/ja.css +16 -0
  188. package/src/themes/intl/pt.css +16 -0
  189. package/src/themes/intl/zh.css +16 -0
  190. package/src/themes/intl.css +16 -0
  191. package/src/themes/monokai.css +22 -0
  192. package/src/themes/phosphor.css +5 -0
  193. package/src/themes/pro-dark.css +10 -0
  194. package/src/themes/solarized-dark.css +7 -0
  195. package/src/themes/solarized.css +7 -0
  196. package/src/themes/vaporwave.css +20 -0
  197. package/src/ts/column-format.ts +67 -46
  198. package/src/ts/perspective-viewer.ts +3 -0
  199. package/src/ts/plugin.ts +14 -0
  200. package/src/ts/providers.ts +96 -0
  201. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  202. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  203. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  204. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  205. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  206. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  207. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  208. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  209. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  210. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  211. package/src/ts/ts-rs/FormatMode.ts +3 -0
  212. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  213. package/src/ts/ts-rs/Notation.ts +11 -0
  214. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  215. package/src/ts/ts-rs/PanelViewerConfig.ts +48 -2
  216. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  217. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  218. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  219. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  220. package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
  221. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  222. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  223. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  224. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  225. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  226. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  227. package/src/ts/ts-rs/Unit.ts +3 -0
  228. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  229. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  230. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  231. package/src/ts/ts-rs/ViewerConfig.ts +48 -2
  232. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  233. package/src/ts/ts-rs/ViewerConfigUpdate.ts +72 -2
  234. package/src/ts/ts-rs/WindowSpec.ts +3 -2
  235. package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
  236. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +18 -9
  237. package/dist/esm/ts-rs/WindowAggregate.d.ts +0 -1
  238. package/src/rust/components/form/color_range_selector.rs +0 -137
  239. package/src/ts/ts-rs/WindowAggregate.ts +0 -3
  240. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline0.js +0 -0
  241. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline1.js +0 -0
  242. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline2.js +0 -0
  243. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline3.js +0 -0
  244. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline4.js +0 -0
  245. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline5.js +0 -0
@@ -142,19 +142,14 @@ pub struct RendererData {
142
142
  /// coexist there.
143
143
  slot_name: RefCell<Option<String>>,
144
144
 
145
- /// This panel's selected theme name (per-panel theming), or `None` to
146
- /// inherit the element-level (active) theme.
145
+ /// This panel's theme name — CONCRETE, resolved once at creation from
146
+ /// the config's `theme`, else the host's, else the registry default.
147
+ /// There is no "unthemed panel" that resolves against live element
148
+ /// state at draw time: a registry re-ordering must never repaint a
149
+ /// panel, so nothing but an explicit write may change this. `None` only
150
+ /// while no themes exist at all.
147
151
  theme: RefCell<Option<String>>,
148
152
 
149
- /// The registry default theme name (first registered), cached here so
150
- /// LOCKED draw paths can resolve this panel's EFFECTIVE theme
151
- /// synchronously (`Presentation::get_default_theme_name` is async — it
152
- /// awaits theme discovery). Seeded by every content-load path before its
153
- /// first locked draw (`restore_and_render`, `load()`, the
154
- /// resize-observer's deferred first render) and kept fresh by the root's
155
- /// `UpdatePresentation` default-theme fan-out and `resetThemes`.
156
- default_theme: RefCell<Option<String>>,
157
-
158
153
  /// Whether the active plugin has completed a draw. An EXPLICIT flag —
159
154
  /// not inferred from DOM connectedness — because plugin elements may be
160
155
  /// mounted eagerly (at panel creation / draw start, before the view
@@ -276,7 +271,6 @@ impl Renderer {
276
271
  on_render_limits_changed: Default::default(),
277
272
  slot_name: Default::default(),
278
273
  theme: Default::default(),
279
- default_theme: Default::default(),
280
274
  has_drawn: Cell::new(false),
281
275
  captured_theme: Default::default(),
282
276
  cached_context: Default::default(),
@@ -299,45 +293,24 @@ impl Renderer {
299
293
  self.0.slot_name.borrow().clone()
300
294
  }
301
295
 
302
- /// Set this panel's theme name (`None` = inherit the element-level theme).
296
+ /// Set this panel's theme name. Callers must pass a CONCRETE name —
297
+ /// resolve "the default" through `Presentation` before calling, never by
298
+ /// leaving this empty.
303
299
  pub fn set_theme(&self, name: Option<String>) {
304
300
  *self.0.theme.borrow_mut() = name;
305
301
  }
306
302
 
307
- /// This panel's selected theme name, if any (per-panel theming).
303
+ /// This panel's theme name.
308
304
  pub fn theme(&self) -> Option<String> {
309
305
  self.0.theme.borrow().clone()
310
306
  }
311
307
 
312
- /// [`Self::set_theme`] plus a synchronous [`Self::stamp_theme`] when the
313
- /// effective theme is resolvable now (a named theme needs no registry; a
314
- /// reset stamps only from a warm default cache — a cold one would stamp
315
- /// attribute-removal). The shared "stamp-with-commit" head of every
316
- /// per-panel theme mutation site.
308
+ /// [`Self::set_theme`] plus a synchronous [`Self::stamp_theme`] — the
309
+ /// shared "stamp-with-commit" head of every per-panel theme mutation
310
+ /// site.
317
311
  pub fn set_theme_stamped(&self, theme: Option<String>) {
318
- let stamp = theme.is_some() || self.default_theme().is_some();
319
312
  self.set_theme(theme);
320
- if stamp {
321
- self.stamp_theme(None);
322
- }
323
- }
324
-
325
- /// Set the cached registry default theme name (see the field docs on
326
- /// [`RendererData`]).
327
- pub fn set_default_theme(&self, name: Option<String>) {
328
- *self.0.default_theme.borrow_mut() = name;
329
- }
330
-
331
- /// The cached registry default theme name.
332
- pub fn default_theme(&self) -> Option<String> {
333
- self.0.default_theme.borrow().clone()
334
- }
335
-
336
- /// This panel's EFFECTIVE theme: its own ([`Self::theme`]), else the
337
- /// cached registry default ([`Self::default_theme`]) — the value
338
- /// [`Self::stamp_theme`] stamps.
339
- pub fn effective_theme(&self) -> Option<String> {
340
- self.theme().or_else(|| self.default_theme())
313
+ self.stamp_theme(None);
341
314
  }
342
315
 
343
316
  /// Whether the active plugin's captured `--psp-*` CSS is STALE — the
@@ -352,7 +325,7 @@ impl Renderer {
352
325
  /// paint captures fresh by construction ("stamp before draw").
353
326
  pub fn needs_restyle(&self) -> bool {
354
327
  match &*self.0.captured_theme.borrow() {
355
- Some(captured) => *captured != self.effective_theme(),
328
+ Some(captured) => *captured != self.theme(),
356
329
  None => false,
357
330
  }
358
331
  }
@@ -20,11 +20,16 @@ use crate::config::PluginStaticConfig;
20
20
 
21
21
  #[extend::ext]
22
22
  pub impl ViewConfigUpdate {
23
- /// Coerce this update's `group_rollup_mode` to one the plugin accepts
24
- /// (`PluginStaticConfig::group_rollup_modes`, feature-filtered). An
25
- /// absent mode counts as `Rollup` for the acceptance check, so a plugin
26
- /// that only renders `flat` (Treemap / Sunburst) gets it stamped even
27
- /// when the update never mentions rollup at all.
23
+ /// Coerce this update's `group_rollup_mode` / `split_rollup_mode` to one
24
+ /// the plugin accepts (`PluginStaticConfig::*_rollup_modes`,
25
+ /// feature-filtered). An absent mode counts as `current`'s committed mode
26
+ /// for the acceptance check — `ViewConfig::apply_update` KEEPS the
27
+ /// committed mode when the update's field is `None`, so acceptance must
28
+ /// be judged against the mode that will actually be in effect. A
29
+ /// hardcoded-default fallback here let a plugin swap silently retain a
30
+ /// mode the new plugin excludes (e.g. Datagrid's `split_rollup_mode:
31
+ /// "rollup"` surviving a `restore({plugin: "Y Bar"})` onto a flat-only
32
+ /// chart).
28
33
  ///
29
34
  /// Split out of [`Self::set_update_column_defaults`] so same-plugin
30
35
  /// restores can enforce the mode WITHOUT the column-defaulting below —
@@ -33,6 +38,7 @@ pub impl ViewConfigUpdate {
33
38
  fn set_update_rollup_defaults(
34
39
  &mut self,
35
40
  metadata: &SessionMetadata,
41
+ current: &ViewConfig,
36
42
  config_static: &PluginStaticConfig,
37
43
  ) {
38
44
  let rollup_features = metadata
@@ -44,7 +50,7 @@ pub impl ViewConfigUpdate {
44
50
  if !group_rollups.contains(
45
51
  self.group_rollup_mode
46
52
  .as_ref()
47
- .unwrap_or(&GroupRollupMode::Rollup),
53
+ .unwrap_or(&current.group_rollup_mode),
48
54
  ) {
49
55
  self.group_rollup_mode = group_rollups.first().cloned();
50
56
  tracing::debug!(
@@ -52,6 +58,24 @@ pub impl ViewConfigUpdate {
52
58
  self.group_rollup_mode
53
59
  );
54
60
  }
61
+
62
+ let split_rollup_features = metadata
63
+ .get_features()
64
+ .map(|x| x.get_split_rollup_modes())
65
+ .unwrap_or_default();
66
+
67
+ let split_rollups = config_static.get_split_rollups(&split_rollup_features);
68
+ if !split_rollups.contains(
69
+ self.split_rollup_mode
70
+ .as_ref()
71
+ .unwrap_or(&current.split_rollup_mode),
72
+ ) {
73
+ self.split_rollup_mode = split_rollups.first().cloned();
74
+ tracing::debug!(
75
+ "Setting plugin-advised split rollup mode {:?}",
76
+ self.split_rollup_mode
77
+ );
78
+ }
55
79
  }
56
80
 
57
81
  /// Appends additional columns to the `columns` field of this
@@ -63,10 +87,11 @@ pub impl ViewConfigUpdate {
63
87
  fn set_update_column_defaults(
64
88
  &mut self,
65
89
  metadata: &SessionMetadata,
90
+ current: &ViewConfig,
66
91
  columns: &[Option<String>],
67
92
  config_static: &PluginStaticConfig,
68
93
  ) {
69
- self.set_update_rollup_defaults(metadata, config_static);
94
+ self.set_update_rollup_defaults(metadata, current, config_static);
70
95
 
71
96
  if let (None, Some(min_cols)) = (&self.columns, config_static.min_config_columns) {
72
97
  let names_len = config_static.config_column_names.len();
@@ -98,34 +98,20 @@ impl SessionMetadata {
98
98
 
99
99
  /// Records the pre-aggregation types of the config's window columns, so
100
100
  /// `get_column_table_type` resolves them like any other derived column.
101
- /// Types mirror the engine's `t_window_engine::resolve_dtype` rules from
102
- /// the window's op and its source's table type - independent of any
103
- /// aggregation the view may also apply.
104
101
  pub(super) fn update_windows(
105
102
  &mut self,
106
103
  windows: &perspective_client::config::Windows,
107
104
  ) -> ApiResult<()> {
108
- use perspective_client::config::WindowAggregate;
109
105
  let window_schema = windows
110
106
  .iter()
111
107
  .filter_map(|(name, w)| {
112
108
  let source = self.get_column_table_type(&w.column)?;
113
- let dtype = match w.aggregate {
114
- WindowAggregate::Sum
115
- | WindowAggregate::Avg
116
- | WindowAggregate::Stddev
117
- | WindowAggregate::Var
118
- | WindowAggregate::Rate
119
- | WindowAggregate::Ema
120
- | WindowAggregate::Diff => ColumnType::Float,
121
- WindowAggregate::Count => ColumnType::Integer,
122
- WindowAggregate::Min
123
- | WindowAggregate::Max
124
- | WindowAggregate::First
125
- | WindowAggregate::Last
126
- | WindowAggregate::Lag
127
- | WindowAggregate::Lead => source,
128
- };
109
+ let dtype = self
110
+ .get_window_aggregate(source, &w.aggregate)
111
+ .and_then(|spec| spec.result_type)
112
+ .and_then(|ty| ColumnType::try_from(ty).ok())
113
+ .unwrap_or(source);
114
+
129
115
  Some((name.clone(), dtype))
130
116
  })
131
117
  .collect();
@@ -133,6 +119,18 @@ impl SessionMetadata {
133
119
  Ok(())
134
120
  }
135
121
 
122
+ /// The declaration for one window aggregate over a `source` column type.
123
+ pub fn get_window_aggregate(
124
+ &self,
125
+ source: ColumnType,
126
+ name: &str,
127
+ ) -> Option<perspective_client::proto::WindowAggregateArgs> {
128
+ self.get_features()?
129
+ .get_window_aggregates(source)
130
+ .into_iter()
131
+ .find(|x| x.name == name)
132
+ }
133
+
136
134
  /// Whether `view_schema` has been populated by a prior successful
137
135
  /// `create_view`. Used by `Session::create_view` to force a full
138
136
  /// build on first run even when `is_clean` happens to be true, so
@@ -189,7 +187,7 @@ impl SessionMetadata {
189
187
  }
190
188
 
191
189
  pub fn get_expression_columns(&self) -> impl Iterator<Item = &'_ String> {
192
- try {
190
+ let mut columns = try {
193
191
  self.as_ref()?
194
192
  .expr_meta
195
193
  .as_ref()?
@@ -199,6 +197,10 @@ impl SessionMetadata {
199
197
  }
200
198
  .into_iter()
201
199
  .flatten()
200
+ .collect::<Vec<_>>();
201
+
202
+ columns.sort_unstable();
203
+ columns.into_iter()
202
204
  }
203
205
 
204
206
  /// Returns the full original expression `String` for an expression alias.
@@ -145,6 +145,7 @@ impl SessionProps {
145
145
  ) {
146
146
  config_update.set_update_column_defaults(
147
147
  &self.metadata,
148
+ &self.config,
148
149
  &self.all_columns().into_iter().map(Some).collect::<Vec<_>>(),
149
150
  config_static,
150
151
  )
@@ -126,6 +126,7 @@ pub impl ViewConfig {
126
126
  filter_op: None,
127
127
  group_by_depth: None,
128
128
  group_rollup_mode: None,
129
+ split_rollup_mode: None,
129
130
  windows: None,
130
131
  }
131
132
  }
@@ -719,9 +719,11 @@ impl Session {
719
719
  config_static: &PluginStaticConfig,
720
720
  ) {
721
721
  use self::column_defaults_update::*;
722
+ let config = self.get_view_config();
722
723
  config_update.set_update_column_defaults(
723
724
  &self.metadata(),
724
- &self.get_view_config().columns,
725
+ &config,
726
+ &config.columns,
725
727
  config_static,
726
728
  )
727
729
  }
@@ -737,7 +739,11 @@ impl Session {
737
739
  config_static: &PluginStaticConfig,
738
740
  ) {
739
741
  use self::column_defaults_update::*;
740
- config_update.set_update_rollup_defaults(&self.metadata(), config_static)
742
+ config_update.set_update_rollup_defaults(
743
+ &self.metadata(),
744
+ &self.get_view_config(),
745
+ config_static,
746
+ )
741
747
  }
742
748
 
743
749
  /// Apply a `ViewConfigUpdate` to the live config — the ONLY view-config
@@ -889,7 +895,7 @@ impl Session {
889
895
  .pending_dispatches
890
896
  .set(self.0.pending_dispatches.get() + 1);
891
897
  let session = self.clone();
892
- ApiFuture::spawn(async move {
898
+ ApiFuture::spawn_named("config-update-dispatch", async move {
893
899
  let result = fut.await;
894
900
  let remaining = session.0.pending_dispatches.get() - 1;
895
901
  session.0.pending_dispatches.set(remaining);
@@ -1165,9 +1171,6 @@ impl Session {
1165
1171
  let old = self.borrow_mut().view_sub.take();
1166
1172
  ApiFuture::spawn(old.delete());
1167
1173
  self.borrow_mut().view_sub = Some(sub);
1168
- // REBUILD: a `View` was genuinely constructed and bound — the one
1169
- // place `view_created` may fire, and the one place the `FreshView`
1170
- // full-draw witness is minted from a rebuild.
1171
1174
  self.view_created.emit(());
1172
1175
  self.commit_reconciled.emit(());
1173
1176
  match self.get_view() {
@@ -20,7 +20,7 @@ use crate::renderer::*;
20
20
  use crate::session::{ResetOptions, Session};
21
21
  use crate::tasks::*;
22
22
  use crate::utils::*;
23
- use crate::workspace::{Panel, PanelId, Workspace};
23
+ use crate::workspace::{Panel, PanelId, PanelPhase, Workspace};
24
24
  use crate::*;
25
25
 
26
26
  /// Build the full set of subscriptions a [`Panel`] owns for its lifetime: its
@@ -69,20 +69,20 @@ fn wire_panel_render_sub(session: &Session, renderer: &Renderer) -> Subscription
69
69
  })
70
70
  }
71
71
 
72
- /// Create a new independent panel (own `Session` + `Renderer` + id) from a
73
- /// `ViewerConfigUpdate`, mount and draw it, and return its id. Shared by
74
- /// `addPanel`, whole-element `restore`, and `restorePanel`'s create-if-missing
75
- /// path. `id` is the panel's id — provided when restoring into a specific named
76
- /// slot, or `None` to generate a fresh one. `settings`/`theme` are stripped
77
- /// (element-level, not per-panel) and `client` — or the element's default
78
- /// client when `None` — is bound so the config's `table` resolves against it.
72
+ /// Create a new independent panel (own `Session` + `Renderer` + id) and
73
+ /// restore `config` into it. Shared by `addPanel` and `restore`'s
74
+ /// create-if-missing upsert. `id` is the panel's id — provided when
75
+ /// restoring into a specific named slot, or `None` to generate a fresh
76
+ /// one. `theme` is stripped (element-level, not per-panel) and `client` —
77
+ /// or the element's default client when `None` — is bound so the config's
78
+ /// `table` resolves against it.
79
79
  pub(crate) async fn create_panel(
80
80
  elem: &HtmlElement,
81
81
  presentation: &Presentation,
82
82
  workspace: &Workspace,
83
83
  notify: &Callback<()>,
84
84
  id: Option<PanelId>,
85
- update: ViewerConfigUpdate,
85
+ config: ViewerConfigInitial,
86
86
  client: Option<perspective_client::Client>,
87
87
  ) -> ApiResult<PanelId> {
88
88
  let (id, session, renderer, update) = create_panel_model(
@@ -90,36 +90,25 @@ pub(crate) async fn create_panel(
90
90
  presentation,
91
91
  workspace,
92
92
  id,
93
- update,
93
+ config.into(),
94
94
  client,
95
- Placement::Placed,
95
+ Placement::Staged,
96
96
  );
97
97
 
98
98
  stamp_global_overlay(workspace, &id, &session);
99
- // STAGE the panel (model-first): `MainPanel` renders its plugin slot
100
- // into a hidden pre-sized wrapper — not a layout cell — so the restore
101
- // below completes its first draw invisibly at (near-)final size. The
102
- // promote below then re-renders, and `MainPanel::reconcile` inserts an
103
- // ALREADY-DRAWN panel: the one layout transition reveals content,
104
- // never a blank frame.
105
- workspace.stage_panel(&id);
106
99
  notify.emit(());
107
100
 
108
- // The staging deadline: a slow restore promotes EARLY (placed, still
109
- // loading — the pre-staging progressive reveal) rather than leaving
110
- // the layout without feedback. Races the completion promote below on
111
- // the staged flag; only the winner re-renders.
112
- {
101
+ ApiFuture::spawn({
113
102
  clone!(workspace, id, notify);
114
- ApiFuture::spawn(async move {
103
+ async move {
115
104
  set_timeout(STAGING_DEADLINE_MS).await?;
116
- if workspace.clear_staged(&id) {
105
+ if workspace.promote(&id) {
117
106
  notify.emit(());
118
107
  }
119
108
 
120
109
  Ok(())
121
- });
122
- }
110
+ }
111
+ });
123
112
 
124
113
  // A fresh panel is never the active one, so it needs no `root` for the
125
114
  // (active-only) settings-sidebar sequencing.
@@ -128,15 +117,15 @@ pub(crate) async fn create_panel(
128
117
  &renderer,
129
118
  presentation,
130
119
  workspace,
131
- None,
132
120
  RestoreMode::Fresh,
133
121
  update,
122
+ crate::tasks::RestoreErrors::Publish,
134
123
  )
135
124
  .await;
136
125
 
137
126
  // Promote on completion AND error — a failed restore's error state
138
127
  // must become visible too.
139
- if workspace.clear_staged(&id) {
128
+ if workspace.promote(&id) {
140
129
  notify.emit(());
141
130
  }
142
131
 
@@ -146,10 +135,17 @@ pub(crate) async fn create_panel(
146
135
 
147
136
  /// Where [`create_panel_model`] registers the new panel model.
148
137
  pub(crate) enum Placement {
149
- /// Into the placed panel set ([`Workspace::insert_panel`]) — a live,
150
- /// visible panel from the start.
138
+ /// Into the placed panel set at [`PanelPhase::Placed`] — a live, visible
139
+ /// panel from the start (`restoreWorkspace` mounts panels directly at
140
+ /// their saved layout positions).
151
141
  Placed,
152
142
 
143
+ /// Into the placed panel set at [`PanelPhase::Staging`] — withheld from
144
+ /// the layout while its first draw completes in the hidden staging
145
+ /// wrapper, then promoted ([`Workspace::promote`]) by restore completion
146
+ /// or the [`STAGING_DEADLINE_MS`] deadline, whichever wins.
147
+ Staged,
148
+
153
149
  /// Into the reservation slot ([`Workspace::reserve_panel`]) — a pending
154
150
  /// `load()`'s first panel, held out of the placed set until it is
155
151
  /// claimed ([`place_reserved`]) or discarded.
@@ -170,32 +166,23 @@ pub(crate) fn create_panel_model(
170
166
  client: Option<perspective_client::Client>,
171
167
  placement: Placement,
172
168
  ) -> (PanelId, Session, Renderer, ViewerConfigUpdate) {
173
- // Authored-theme boot: the FIRST panel of an empty element with no explicit
174
- // theme adopts the element's `theme` attribute (a one-time initial
175
- // selection; the attribute is otherwise an active-panel mirror). Captured
176
- // before `insert_panel` makes the workspace non-empty.
177
- let boot_theme = (workspace.is_empty() && matches!(update.theme, OptionalUpdate::Missing))
178
- .then(|| elem.get_attribute("theme"))
179
- .flatten();
180
-
181
169
  let session = Session::new();
182
170
  let renderer = Renderer::new(elem);
183
171
  let id = id.unwrap_or_else(|| workspace.generate_id());
184
172
  renderer.set_slot_name(id.as_str());
185
- renderer.set_default_theme(presentation.default_theme_name_sync());
186
173
  let subs = wire_panel_subs(elem, presentation, &session, &renderer);
187
174
  let panel = Panel::new(id.clone(), session.clone(), renderer.clone(), subs);
188
175
  match placement {
189
- Placement::Placed => workspace.insert_panel(panel),
176
+ Placement::Placed => workspace.insert_panel(panel, PanelPhase::Placed),
177
+ Placement::Staged => workspace.insert_panel(panel, PanelPhase::Staging),
190
178
  Placement::Reserved => workspace.reserve_panel(panel),
191
179
  }
192
180
 
193
181
  update.settings = OptionalUpdate::Missing;
194
- if let OptionalUpdate::Update(theme) = &update.theme {
195
- renderer.set_theme(Some(theme.clone()));
196
- } else if let Some(theme) = boot_theme {
197
- renderer.set_theme(Some(theme));
198
- }
182
+ renderer.set_theme(match &update.theme {
183
+ OptionalUpdate::Update(theme) => Some(theme.clone()),
184
+ _ => presentation.active_theme_name_sync(),
185
+ });
199
186
 
200
187
  update.theme = OptionalUpdate::Missing;
201
188
  if let Some(client) = client.or_else(|| workspace.default_client()) {
@@ -211,8 +198,18 @@ pub(crate) fn create_panel_model(
211
198
  (id, session, renderer, update)
212
199
  }
213
200
 
214
- pub(crate) fn place_reserved(workspace: &Workspace, notify: &Callback<()>) -> Option<Panel> {
215
- let panel = workspace.claim_reserved()?;
201
+ /// Claim the pending `load()` reservation into the placed set
202
+ /// ([`Workspace::claim_reserved`]), stamping the global-filter overlay and
203
+ /// notifying the root. `has_table` is whether the claimant carried a
204
+ /// `table`, recorded atomically with the claim — pass `true` when `load()`
205
+ /// itself places its reservation (a `Table` payload, or error surfacing),
206
+ /// which is not a restore claim and must never arm the epilogue's eviction.
207
+ pub(crate) fn place_reserved(
208
+ workspace: &Workspace,
209
+ notify: &Callback<()>,
210
+ has_table: bool,
211
+ ) -> Option<Panel> {
212
+ let panel = workspace.claim_reserved(has_table)?;
216
213
  stamp_global_overlay(workspace, &panel.id, &panel.session);
217
214
  notify.emit(());
218
215
  Some(panel)
@@ -220,7 +217,7 @@ pub(crate) fn place_reserved(workspace: &Workspace, notify: &Callback<()>) -> Op
220
217
 
221
218
  /// Tear down a [`Panel`] already removed from the [`Workspace`]: dispose its
222
219
  /// renderer (slot-scoped plugin + light-DOM cleanup) and eject its table.
223
- /// Shared by the root's `ClosePanel` handler and whole-element `restore`'s
220
+ /// Shared by the root's `ClosePanel` handler and `restoreWorkspace`'s
224
221
  /// batch replacement of the pre-existing panel set.
225
222
  pub(crate) fn eject_panel(panel: Panel) -> ApiFuture<()> {
226
223
  let was_errored = panel.session.is_errored();
@@ -18,14 +18,22 @@ use perspective_client::clone;
18
18
  use perspective_client::config::{Expression, ViewConfigUpdate};
19
19
 
20
20
  use super::apply_and_render;
21
- use crate::presentation::{ColumnLocator, ColumnSettingsTab, OpenColumnSettings, Presentation};
21
+ use crate::presentation::{
22
+ ColumnSettingsTab, ColumnSettingsTarget, OpenColumnSettings, Presentation,
23
+ };
22
24
  use crate::renderer::Renderer;
23
25
  use crate::session::Session;
24
26
  use crate::*;
25
27
 
26
- /// Replace the expression at `old_name` with `new_expr` and re-render. After
27
- /// the render completes, opens the column-settings drawer on the renamed
28
- /// expression.
28
+ fn reopen(presentation: &Presentation, name: String) {
29
+ presentation.set_open_column_settings(Some(OpenColumnSettings {
30
+ target: Some(ColumnSettingsTarget::Column(name)),
31
+ tab: Some(ColumnSettingsTab::Attributes),
32
+ }));
33
+ }
34
+
35
+ /// Replace the expression at `old_name` with `new_expr` and re-render,
36
+ /// moving the column-settings drawer onto the renamed expression.
29
37
  pub fn update_expr(
30
38
  session: &Session,
31
39
  renderer: &Renderer,
@@ -39,25 +47,21 @@ pub fn update_expr(
39
47
  .to_props()
40
48
  .create_replace_expression_update(&old_name, &new_expr);
41
49
 
42
- apply_and_render(&session, &renderer, update)?.await?;
43
- presentation.set_open_column_settings(Some(OpenColumnSettings {
44
- locator: Some(ColumnLocator::Expression(new_expr.name.to_string())),
45
- tab: Some(ColumnSettingsTab::Attributes),
46
- }));
47
-
50
+ let run = apply_and_render(&session, &renderer, update)?;
51
+ reopen(&presentation, new_expr.name.to_string());
52
+ run.await?;
48
53
  Ok(())
49
54
  });
50
55
  }
51
56
 
52
- /// Insert `expr` into the session's expressions and re-render. After the
53
- /// render completes, opens the column-settings drawer on the new expression.
57
+ /// Insert `expr` into the session's expressions and re-render, moving the
58
+ /// column-settings drawer onto the new expression.
54
59
  pub fn save_expr(
55
60
  session: &Session,
56
61
  renderer: &Renderer,
57
62
  presentation: &Presentation,
58
63
  expr: Expression,
59
64
  ) -> ApiResult<()> {
60
- let presentation = presentation.clone();
61
65
  let expr_name: String = expr.name.clone().into();
62
66
  let task = {
63
67
  let mut serde_exprs = session.get_view_config().expressions.clone();
@@ -68,16 +72,8 @@ pub fn save_expr(
68
72
  })
69
73
  }?;
70
74
 
71
- ApiFuture::spawn(async move {
72
- task.await?;
73
- presentation.set_open_column_settings(Some(OpenColumnSettings {
74
- locator: Some(ColumnLocator::Expression(expr_name)),
75
- tab: Some(ColumnSettingsTab::Attributes),
76
- }));
77
-
78
- Ok(())
79
- });
80
-
75
+ reopen(presentation, expr_name);
76
+ ApiFuture::spawn(task);
81
77
  Ok(())
82
78
  }
83
79
 
@@ -14,14 +14,16 @@ use perspective_client::clone;
14
14
  use perspective_client::config::{Filter, ViewConfigUpdate, WindowSpec};
15
15
 
16
16
  use super::apply_and_render;
17
- use crate::presentation::{ColumnLocator, ColumnSettingsTab, OpenColumnSettings, Presentation};
17
+ use crate::presentation::{
18
+ ColumnSettingsTab, ColumnSettingsTarget, OpenColumnSettings, Presentation,
19
+ };
18
20
  use crate::renderer::Renderer;
19
21
  use crate::session::Session;
20
22
  use crate::*;
21
23
 
22
24
  fn reopen(presentation: &Presentation, name: String) {
23
25
  presentation.set_open_column_settings(Some(OpenColumnSettings {
24
- locator: Some(ColumnLocator::Window(name)),
26
+ target: Some(ColumnSettingsTarget::Column(name)),
25
27
  tab: Some(ColumnSettingsTab::Window),
26
28
  }));
27
29
  }
@@ -36,7 +38,6 @@ pub fn save_window(
36
38
  name: String,
37
39
  spec: WindowSpec,
38
40
  ) -> ApiResult<()> {
39
- let presentation = presentation.clone();
40
41
  let task = {
41
42
  let mut windows = session.get_view_config().windows.clone();
42
43
  windows.insert(name.clone(), spec);
@@ -46,12 +47,8 @@ pub fn save_window(
46
47
  })
47
48
  }?;
48
49
 
49
- ApiFuture::spawn(async move {
50
- task.await?;
51
- reopen(&presentation, name);
52
- Ok(())
53
- });
54
-
50
+ reopen(presentation, name);
51
+ ApiFuture::spawn(task);
55
52
  Ok(())
56
53
  }
57
54
 
@@ -93,7 +90,7 @@ pub fn update_window(
93
90
  config
94
91
  .columns
95
92
  .iter()
96
- .map(|col| col.as_ref().map(&rename))
93
+ .map(|col| col.as_ref().map(rename))
97
94
  .collect(),
98
95
  );
99
96
  update.group_by = Some(config.group_by.iter().map(&rename).collect());
@@ -134,8 +131,9 @@ pub fn update_window(
134
131
  update
135
132
  };
136
133
 
137
- apply_and_render(&session, &renderer, update)?.await?;
134
+ let run = apply_and_render(&session, &renderer, update)?;
138
135
  reopen(&presentation, new_name);
136
+ run.await?;
139
137
  Ok(())
140
138
  });
141
139
  }
@@ -23,11 +23,14 @@ use crate::workspace::Workspace;
23
23
  /// Tear down the entire viewer: dispose EVERY panel's engines (emit
24
24
  /// `table_unloaded`, destroy the renderer under its own draw lock, eject the
25
25
  /// session), then drop the Yew root once. Fans out over all panels — not just
26
- /// the seed — so panels added via `addPanel` or a whole-element `restore` don't
26
+ /// the seed — so panels added via `addPanel` or `restoreWorkspace` don't
27
27
  /// leak their `View`/`Table` + plugin when the element is deleted or ejected.
28
28
  pub fn delete_all<T: Component>(workspace: &Workspace, root: &Root<T>) -> ApiFuture<()> {
29
29
  clone!(workspace, root);
30
30
  ApiFuture::new(async move {
31
+ // A staged-but-unapplied `restoreWorkspace` layout tree must not
32
+ // outlive the panels it names.
33
+ workspace.take_pending_layout();
31
34
  let panels = workspace
32
35
  .take_reserved()
33
36
  .into_iter()