@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
@@ -285,12 +285,12 @@ pub(crate) async fn locked_run(
285
285
 
286
286
  let view_config_snapshot = session.get_view_config().clone();
287
287
  let plugin_config_changed =
288
- renderer.update_plugin_config(&view_config_snapshot, spec.plugin_config);
288
+ renderer.update_plugin_config(&view_config_snapshot, spec.plugin_config)?;
289
289
  let columns_config_changed = renderer.update_columns_configs(
290
290
  &view_config_snapshot,
291
291
  &session,
292
292
  spec.columns_config,
293
- );
293
+ )?;
294
294
 
295
295
  let changed = plugin_config_changed || columns_config_changed;
296
296
  if changed || plugin_swapped {
@@ -131,11 +131,14 @@ pub async fn presize_visible_panels_grown(
131
131
  presize_visible_panels_scaled(workspace, elem, width_ratio, height_ratio).await
132
132
  }
133
133
 
134
- /// Pre-size each visible plugin to its SHRUNK post-open cell *before* the
135
- /// settings pane mounts (P2), using the open-state geometry deltas cached at
136
- /// the last close (`(layout_area.w − mpc.w, main_column.h − mpc.h)` — the pane
137
- /// and divider width and the docked status-bar height, both stable across a
138
- /// close/open cycle since the pane width persists in the override).
134
+ /// Pre-size each visible plugin to its cell SHRUNK by `(delta_w, delta_h)` —
135
+ /// GROWN when a delta is negative — *before* the layout change commits. Used
136
+ /// by the settings-pane open (P2, deltas `(layout_area.w − mpc.w,
137
+ /// main_column.h − mpc.h)` cached at the last close — the pane and divider
138
+ /// width and the docked status-bar height, both stable across a close/open
139
+ /// cycle since the pane width persists in the override) and by the
140
+ /// column-settings pin toggle and docked-drawer mount/unmount (signed delta
141
+ /// measured live at toggle time).
139
142
  pub async fn presize_visible_panels_open(
140
143
  workspace: &Workspace,
141
144
  elem: &web_sys::HtmlElement,
@@ -199,6 +202,7 @@ async fn presize_visible_panels_scaled(
199
202
  return StagedPresents::default();
200
203
  }
201
204
 
205
+ let mpc = shadow_rect(elem, "#main_panel_container");
202
206
  let mut last_chrome: Option<(f64, f64)> = None;
203
207
  let targets = workspace
204
208
  .panels()
@@ -222,6 +226,23 @@ async fn presize_visible_panels_scaled(
222
226
 
223
227
  last_chrome = chrome.or(last_chrome);
224
228
  let (cw, ch) = last_chrome.unwrap_or(super::CHROME_FALLBACK);
229
+
230
+ // Per axis, chrome can never exceed the container's
231
+ // non-plugin remainder: with no sibling panel on that axis,
232
+ // `mpc − box` IS the exact chrome, while the frame-margin
233
+ // measurement overcounts (margins that overlap container
234
+ // padding) — the source of a ~2px target miss that pushed
235
+ // every commit past the plugin transport's ±0.5px resize
236
+ // dedupe, costing an extra corrective render. With siblings
237
+ // the remainder is large and the measured chrome wins.
238
+ let (cw, ch) = match &mpc {
239
+ Some(mpc) => (
240
+ cw.min((mpc.width() - plugin_box.width()).max(0.0)),
241
+ ch.min((mpc.height() - plugin_box.height()).max(0.0)),
242
+ ),
243
+ None => (cw, ch),
244
+ };
245
+
225
246
  let w = ((plugin_box.width() + cw) * width_ratio - cw).max(0.0);
226
247
  let h = ((plugin_box.height() + ch) * height_ratio - ch).max(0.0);
227
248
  Some((panel, w, h))
@@ -300,6 +321,24 @@ pub fn measure_settings_open_deltas(elem: &web_sys::HtmlElement) -> Option<(f64,
300
321
  ))
301
322
  }
302
323
 
324
+ /// The signed `#main_panel_container` width delta a column-settings pin
325
+ /// toggle will cause: the docked `#modal_panel`'s flex box spans its drawer
326
+ /// child plus the overlapping divider (`drawer.right − divider.left`, the
327
+ /// same span in both modes since `.pinned` reparents nothing) — PINNING
328
+ /// takes it from the main column (positive, shrink), UNPINNING releases it
329
+ /// back (negative, grow). `None` when the drawer isn't mounted.
330
+ pub fn measure_column_settings_pin_delta(
331
+ elem: &web_sys::HtmlElement,
332
+ is_pinned: bool,
333
+ ) -> Option<f64> {
334
+ let drawer = shadow_rect(elem, "#modal_panel > .split-panel-child:first-child")?;
335
+ let divider_left = shadow_rect(elem, "#modal_panel > .split-panel-divider")
336
+ .map(|r| r.left())
337
+ .unwrap_or_else(|| drawer.left());
338
+ let width = (drawer.right() - divider_left).max(0.0);
339
+ Some(if is_pinned { -width } else { width })
340
+ }
341
+
303
342
  /// The frame chrome a plugin's grid cell has that the plugin itself doesn't
304
343
  /// occupy, as `(width, height)` px: margin + border + titlebar. Measured live
305
344
  /// from a `regular-layout-frame` and its plugin — robust to theme/CSS changes —
@@ -61,11 +61,11 @@ pub fn reset_all(
61
61
 
62
62
  presentation.reset_available_themes(None).await;
63
63
  if all {
64
- // Clear this panel's own per-panel theme so it reverts to inheriting
65
- // the (reset-to-default) host theme — `reset_theme` only resets the
66
- // host, which an explicitly-themed panel would otherwise override.
67
- renderer.set_theme(None);
64
+ // Put this panel back on the registry default CONCRETELY —
65
+ // `reset_theme` only resets the host, which an explicitly-themed
66
+ // panel would otherwise keep overriding.
68
67
  presentation.reset_theme().await?;
68
+ renderer.set_theme(presentation.get_default_theme_name().await);
69
69
  }
70
70
 
71
71
  // For `all = true`, route the bucket clears through `restore_and_render`'s
@@ -18,6 +18,7 @@ use futures::Future;
18
18
  use perspective_client::clone;
19
19
 
20
20
  use super::pipeline::{RunOrigin, RunSpec, locked_run};
21
+ use super::update_theme::seed_panel_theme;
21
22
  use crate::config::{OptionalUpdate, ViewerConfigUpdate};
22
23
  use crate::presentation::Presentation;
23
24
  use crate::renderer::Renderer;
@@ -70,11 +71,6 @@ pub fn restore_and_render(
70
71
  session.set_title(None);
71
72
  }
72
73
 
73
- // Mirror a config-carried theme onto the host attribute (the shared
74
- // chrome). The restyle a theme change requires is owned by the
75
- // MUTATION SITES (`restorePanel`'s own-theme tail, the theme-picker
76
- // task), not here — this run's own draw below already stamps the new
77
- // effective theme before the plugin's first style read.
78
74
  match theme_name {
79
75
  OptionalUpdate::SetDefault => {
80
76
  let current_name = presentation.get_selected_theme_name().await;
@@ -83,59 +79,31 @@ pub fn restore_and_render(
83
79
  }
84
80
  },
85
81
  OptionalUpdate::Update(x) => {
86
- // No pre-resolution gate: `set_theme_name` stamps the host
87
- // attribute SYNCHRONOUSLY before its registry await and
88
- // no-ops on literal equality itself. The old
89
- // `get_selected_theme_name().await` guard was precisely
90
- // the former-theme window on a cold registry — the host
91
- // held the old attribute until stylesheet parsing (and
92
- // everything queued behind it) resolved.
93
82
  presentation.set_theme_name(Some(&x)).await?;
94
83
  },
95
84
  _ => {},
96
85
  };
97
86
 
98
- // Resolve the target plugin here (pure — needed now for
99
- // `set_update_column_defaults`), but COMMIT it only inside the locked
100
- // run below, atomically with the view rebind it belongs to. No swap
101
- // intent may exist outside that run: an unrelated run that wins the
102
- // lock first (e.g. a `table_updated` redraw) must observe either the
103
- // fully-old or fully-new world, never a staged half of this one.
104
87
  let resolved_plugin = renderer.resolve_plugin_update(&plugin);
105
88
  if let Some((_, metadata)) = &resolved_plugin {
106
89
  session.set_update_column_defaults(&mut view_config, metadata);
107
90
  } else {
108
- // Same-plugin (or plugin-less) restore: `resolve_plugin_update`
109
- // returns `None`, but the plugin-advised `group_rollup_mode`
110
- // must STILL be enforced against the active plugin's metadata —
111
- // `restorePanel`'s table-change reset wipes the committed mode,
112
- // and nothing else on this path would restore it, leaving a
113
- // flat-only chart (Treemap / Sunburst) rendering rollup
114
- // subtotal rows. Rollup only; the full column-defaults pass is
115
- // reserved for plugin swaps, where `columns` genuinely needs
116
- // re-defaulting.
117
- session.set_update_rollup_defaults(&mut view_config, &renderer.metadata());
91
+ let metadata = if renderer.active_plugin().is_none() {
92
+ renderer
93
+ .resolve_plugin_update(&OptionalUpdate::SetDefault)
94
+ .map(|(_, metadata)| metadata)
95
+ .unwrap_or_else(|| renderer.metadata())
96
+ } else {
97
+ renderer.metadata()
98
+ };
99
+
100
+ session.set_update_rollup_defaults(&mut view_config, &metadata);
118
101
  }
119
102
 
120
103
  let plugin_idx = resolved_plugin.map(|(idx, _)| idx);
121
-
122
- // The config COMMIT: synchronous, validated, atomic (I1/I4). Under
123
- // I2/I3 committing before the lock is safe — whichever queued run
124
- // snapshots next picks it up, and this run's own snapshot (taken
125
- // inside the lock below) can only be this commit or fresher.
126
104
  session.commit_view_config(view_config)?;
127
-
128
- // Spinner accounting (RAII): held to the end of this restore —
129
- // INCLUDING the deferred-draw exit (no table yet → no
130
- // `bind_snapshot`), which under the old edge-counted scheme
131
- // stranded the `StatusIndicator` spinner permanently.
132
105
  let _run_token = session.begin_config_run();
133
-
134
- // Awaits theme-registry init, so the stamp below can never observe a
135
- // pre-init (empty) theme set on a cold first load. Seeds this
136
- // panel's renderer default-theme cache, which every locked draw
137
- // stamps the effective theme from.
138
- renderer.set_default_theme(presentation.get_default_theme_name().await);
106
+ seed_panel_theme(&presentation, &renderer).await;
139
107
  locked_run(&session, &renderer, RunSpec {
140
108
  origin,
141
109
  plugin_idx,
@@ -146,6 +114,11 @@ pub fn restore_and_render(
146
114
  })
147
115
  .await?;
148
116
 
117
+ if renderer.needs_restyle() {
118
+ renderer.restyle_all().await?;
119
+ }
120
+
121
+ presentation.publish_theme_config().await?;
149
122
  Ok(())
150
123
  })
151
124
  }
@@ -10,15 +10,12 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use futures::channel::oneshot::channel;
14
13
  use perspective_client::clone;
15
14
  use perspective_client::utils::PerspectiveResultExt;
16
15
 
17
- use crate::components::viewer::{PerspectiveViewer, PerspectiveViewerMsg};
18
16
  use crate::config::*;
19
17
  use crate::presentation::Presentation;
20
18
  use crate::renderer::Renderer;
21
- use crate::root::Root;
22
19
  use crate::session::{ResetOptions, Session};
23
20
  use crate::tasks::*;
24
21
  use crate::workspace::Workspace;
@@ -31,6 +28,16 @@ pub(crate) enum RestoreMode {
31
28
  Fresh,
32
29
  }
33
30
 
31
+ /// Where a failed restore's error goes.
32
+ #[derive(Clone, Copy)]
33
+ pub(crate) enum RestoreErrors {
34
+ // Raise errors in the UI.
35
+ Publish,
36
+
37
+ // Report only in the API.
38
+ Suppress,
39
+ }
40
+
34
41
  pub(crate) async fn bind_table_task(
35
42
  session: &Session,
36
43
  workspace: &Workspace,
@@ -50,22 +57,26 @@ pub(crate) async fn bind_table_task(
50
57
  }
51
58
 
52
59
  /// Apply a [`ViewerConfigUpdate`] to a single panel and re-draw — the one
53
- /// pipeline shared by `restorePanel` (an existing panel), whole-element
54
- /// `restoreWorkspace`, and `addPanel` (both fresh panels).
60
+ /// pipeline shared by `restorePanel` (an existing panel), `restoreWorkspace`,
61
+ /// and `addPanel` (both fresh panels).
55
62
  pub(crate) async fn restore_panel(
56
63
  session: &Session,
57
64
  renderer: &Renderer,
58
65
  presentation: &Presentation,
59
66
  workspace: &Workspace,
60
- root: Option<&Root<PerspectiveViewer>>,
61
67
  mode: RestoreMode,
62
68
  mut update: ViewerConfigUpdate,
69
+ errors: RestoreErrors,
63
70
  ) -> ApiResult<()> {
64
71
  let active = matches!(mode, RestoreMode::Existing { active: true });
65
72
  let fresh = matches!(mode, RestoreMode::Fresh);
66
73
  match &update.theme {
67
74
  OptionalUpdate::Update(theme) => renderer.set_theme_stamped(Some(theme.clone())),
68
- OptionalUpdate::SetDefault => renderer.set_theme_stamped(None),
75
+ // `SetDefault` resolves to a CONCRETE registry default here, rather
76
+ // than clearing the panel's theme — nothing downstream re-resolves.
77
+ OptionalUpdate::SetDefault => {
78
+ renderer.set_theme_stamped(presentation.get_default_theme_name().await)
79
+ },
69
80
  OptionalUpdate::Missing => {},
70
81
  }
71
82
 
@@ -77,17 +88,17 @@ pub(crate) async fn restore_panel(
77
88
  tracing::info!("Restoring {update}");
78
89
  }
79
90
 
80
- let (sender, receiver) = channel::<()>();
81
- match (active, root) {
82
- (true, Some(root)) => {
83
- root.borrow().as_ref().into_apierror()?.send_message(
84
- PerspectiveViewerMsg::ToggleSettingsComplete(update.settings.clone(), sender),
85
- );
86
- },
87
- _ => {
88
- let _ = sender.send(());
89
- },
90
- }
91
+ // NOTE: `update.settings` is deliberately NOT applied here. It is
92
+ // element-level chrome rather than panel state, so `restore()` — the
93
+ // only caller that can carry it — applies it before dispatching, and
94
+ // this pipeline stays per-panel. Applying it here reached only the
95
+ // `Existing { active: true }` mode, which is why a freshly created
96
+ // panel silently ignored it.
97
+
98
+ // Under `Suppress` the restore is TRANSACTIONAL, so snapshot the config
99
+ // it is about to overwrite — see the failure tail below for why.
100
+ let rollback =
101
+ matches!(errors, RestoreErrors::Suppress).then(|| session.get_view_config().clone());
91
102
 
92
103
  let table_changed = !fresh
93
104
  && matches!(&update.table, OptionalUpdate::Update(name)
@@ -122,7 +133,6 @@ pub(crate) async fn restore_panel(
122
133
  bind_table_task(&session, &workspace, name).await?;
123
134
  }
124
135
 
125
- receiver.await.unwrap_or_log();
126
136
  Ok(())
127
137
  }
128
138
  },
@@ -130,15 +140,20 @@ pub(crate) async fn restore_panel(
130
140
  .await;
131
141
 
132
142
  if let Err(e) = &result {
133
- session.set_error(false, e.clone()).await?;
143
+ match errors {
144
+ RestoreErrors::Publish => session.set_error(false, e.clone()).await?,
145
+ RestoreErrors::Suppress => {
146
+ if let Some(config) = rollback {
147
+ session.commit_view_config(config.into()).unwrap_or_log();
148
+ }
149
+ },
150
+ }
134
151
  }
135
152
 
136
153
  result?;
137
154
 
138
155
  if fresh {
139
156
  renderer.resize().await.unwrap_or_log();
140
- } else if renderer.needs_restyle() {
141
- renderer.restyle_all().await?;
142
157
  }
143
158
 
144
159
  Ok(())
@@ -34,7 +34,9 @@ pub fn set_edit_mode(session: &Session, renderer: &Renderer, mode: &str) {
34
34
  serde_json::Value::String(mode.to_owned()),
35
35
  );
36
36
 
37
- let changed = renderer.update_plugin_config(&view_config, OptionalUpdate::Update(map));
37
+ let changed = renderer
38
+ .update_plugin_config(&view_config, OptionalUpdate::Update(map))
39
+ .unwrap_or_default();
38
40
  clone!(session, renderer);
39
41
  ApiFuture::spawn(async move {
40
42
  if changed {
@@ -46,8 +46,11 @@ pub fn sync_update_panels(
46
46
  panels,
47
47
  global_filters,
48
48
  masters,
49
+ palette,
49
50
  } = update.into_serde_ext()?;
50
51
 
52
+ let palette = validate_palette(palette.unwrap_or_default())
53
+ .map_err(|e| ApiError::from(JsValue::from_str(&e)))?;
51
54
  let old_ids = this.workspace.panel_ids();
52
55
 
53
56
  // Phase 1 — models only, NO renders and NO draws
@@ -55,20 +58,19 @@ pub fn sync_update_panels(
55
58
  let mut fallback_fresh: Option<String> = None;
56
59
  let mut active_fresh: Option<String> = None;
57
60
  let mut contents = Vec::new();
61
+ // `panels` entries are `ViewerConfigInitial`s — `table` required by
62
+ // TYPE (a placed panel without a binding would be permanently blank),
63
+ // and no per-panel `settings` exists (element-level; carried by the
64
+ // top-level `active` field). An EMPTY `panels` map restores to the
65
+ // zero-panel empty stage — the former table-less fallback panel was
66
+ // exactly the blank-panel state this type exists to preclude.
58
67
  for (saved_id, config) in panels {
59
- if !matches!(config.settings, OptionalUpdate::Missing) {
60
- #[rustfmt::skip]
61
- tracing::warn!(
62
- "`settings` on panel \"{saved_id}\" is ignored in a whole-element config; use the top-level `active` field"
63
- );
64
- }
65
-
66
68
  let (fresh, session, renderer, config) = create_panel_model(
67
69
  &this.elem,
68
70
  &this.presentation,
69
71
  &this.workspace,
70
72
  None,
71
- config,
73
+ config.into(),
72
74
  None,
73
75
  Placement::Placed,
74
76
  );
@@ -82,21 +84,6 @@ pub fn sync_update_panels(
82
84
  contents.push((fresh, session, renderer, config));
83
85
  }
84
86
 
85
- if contents.is_empty() {
86
- let (fresh, session, renderer, config) = create_panel_model(
87
- &this.elem,
88
- &this.presentation,
89
- &this.workspace,
90
- None,
91
- ViewerConfigUpdate::default(),
92
- None,
93
- Placement::Placed,
94
- );
95
-
96
- fallback_fresh = Some(fresh.as_str().to_owned());
97
- contents.push((fresh, session, renderer, config));
98
- }
99
-
100
87
  let mut eject_tasks = Vec::new();
101
88
  if let Some(panel) = this.workspace.take_reserved() {
102
89
  eject_tasks.push(eject_panel(panel));
@@ -108,6 +95,8 @@ pub fn sync_update_panels(
108
95
  }
109
96
  }
110
97
 
98
+ this.presentation.set_palette(palette)?;
99
+
111
100
  // Phase 2 — stage the remapped layout tree on the Workspace
112
101
  if let Some(layout) = layout {
113
102
  this.workspace
@@ -129,13 +118,17 @@ pub fn sync_update_panels(
129
118
  }
130
119
 
131
120
  if let Some(app) = this.root.borrow().as_ref() {
121
+ // Silent (`announce: false`): each restored panel's own view-config
122
+ // commit dispatch announces the config, settings field included.
132
123
  app.send_message(PerspectiveViewerMsg::ToggleSettingsInit(
133
124
  Some(SettingsUpdate::Update(sidebar_open)),
125
+ false,
134
126
  None,
135
127
  ));
136
128
  }
137
129
 
138
130
  let masters = masters
131
+ .unwrap_or_default()
139
132
  .into_iter()
140
133
  .filter_map(|saved| match id_map.get(&saved) {
141
134
  Some(fresh) => Some(PanelId::from(fresh.as_str())),
@@ -147,6 +140,7 @@ pub fn sync_update_panels(
147
140
  .collect::<Vec<_>>();
148
141
 
149
142
  this.workspace.set_masters(masters);
150
- this.workspace.set_global_filters(global_filters);
143
+ this.workspace
144
+ .set_global_filters(global_filters.unwrap_or_default());
151
145
  Ok((contents, eject_tasks))
152
146
  }
@@ -19,20 +19,21 @@ use crate::presentation::Presentation;
19
19
  use crate::renderer::Renderer;
20
20
  use crate::workspace::Workspace;
21
21
 
22
- /// Re-seed every panel renderer's cached registry default theme from the
23
- /// (awaited, so initialized) theme registry, returning the default name.
24
- /// Every consumer of `Renderer::needs_restyle` after a registry-affecting
25
- /// change must run this first — a cold cache compares against `None`.
26
- pub(crate) async fn seed_default_themes(
27
- presentation: &Presentation,
28
- workspace: &Workspace,
29
- ) -> Option<String> {
30
- let default = presentation.get_default_theme_name().await;
31
- for panel in workspace.panels() {
32
- panel.renderer.set_default_theme(default.clone());
22
+ /// Give `renderer` the concrete registry default IF it has none yet — the
23
+ /// cold-boot case, where the panel was created before the registry first
24
+ /// parsed and `active_theme_name_sync` had nothing to resolve. Only ever
25
+ /// FILLS IN, never overwrites, so it cannot repaint a themed panel.
26
+ pub(crate) async fn seed_panel_theme(presentation: &Presentation, renderer: &Renderer) {
27
+ if renderer.theme().is_none() {
28
+ renderer.set_theme(presentation.get_default_theme_name().await);
33
29
  }
30
+ }
34
31
 
35
- default
32
+ /// [`seed_panel_theme`] for every panel.
33
+ pub(crate) async fn seed_default_themes(presentation: &Presentation, workspace: &Workspace) {
34
+ for panel in workspace.panels() {
35
+ seed_panel_theme(presentation, &panel.renderer).await;
36
+ }
36
37
  }
37
38
 
38
39
  /// Apply a theme change and restyle the affected panel's view.
@@ -59,14 +60,17 @@ pub fn update_theme(
59
60
  workspace: &Workspace,
60
61
  theme: Option<String>,
61
62
  ) {
62
- // Per-panel: record the theme on the (active) renderer so this panel keeps
63
- // it independent of which panel is active. `set_theme_name` below mirrors
64
- // the same value onto the host `theme` attribute (driving the chrome), and
65
- // MainPanel inlines this renderer's theme on its frame only when it
66
- renderer.set_theme_stamped(theme.clone());
63
+ // A NAMED theme needs no registry, so it stamps SYNCHRONOUSLY — no await
64
+ // separates the caller's config commit from the attribute the document
65
+ // cascade styles. Only "reset to default" has to await the registry, and
66
+ // it records the resolved NAME, never an empty theme.
67
+ if let Some(name) = &theme {
68
+ renderer.set_theme_stamped(Some(name.clone()));
69
+ }
67
70
 
68
71
  let presentation = presentation.clone();
69
72
  let workspace = workspace.clone();
73
+ let renderer = renderer.clone();
70
74
  ApiFuture::spawn(async move {
71
75
  match theme {
72
76
  Some(name) => {
@@ -74,10 +78,10 @@ pub fn update_theme(
74
78
  },
75
79
  None => {
76
80
  presentation.reset_theme().await?;
81
+ renderer.set_theme_stamped(presentation.get_default_theme_name().await);
77
82
  },
78
83
  }
79
84
 
80
- seed_default_themes(&presentation, &workspace).await;
81
85
  let panels = workspace.panels();
82
86
  join_all(panels.iter().map(|panel| async move {
83
87
  if panel.renderer.needs_restyle() {
@@ -90,6 +94,7 @@ pub fn update_theme(
90
94
  .into_iter()
91
95
  .collect::<ApiResult<Vec<_>>>()?;
92
96
 
97
+ presentation.publish_theme_config().await?;
93
98
  Ok(())
94
99
  });
95
100
  }
@@ -0,0 +1,26 @@
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
+ use web_sys::Element;
14
+
15
+ /// The computed value of custom property `name` on `elem`, trimmed, or
16
+ /// `None` when undefined/empty.
17
+ pub fn read_custom_property(elem: &Element, name: &str) -> Option<String> {
18
+ let value = web_sys::window()?
19
+ .get_computed_style(elem)
20
+ .ok()??
21
+ .get_property_value(name)
22
+ .ok()?;
23
+
24
+ let value = value.trim();
25
+ (!value.is_empty()).then(|| value.to_owned())
26
+ }
@@ -18,6 +18,7 @@
18
18
 
19
19
  mod browser;
20
20
  mod completion;
21
+ mod css_vars;
21
22
  mod custom_element;
22
23
  mod datetime;
23
24
  mod debounce;
@@ -35,6 +36,7 @@ mod tests;
35
36
 
36
37
  pub use browser::*;
37
38
  pub use completion::*;
39
+ pub use css_vars::*;
38
40
  pub use custom_element::*;
39
41
  pub use datetime::*;
40
42
  pub use debounce::*;
@@ -23,7 +23,7 @@ use perspective_js::utils::*;
23
23
  /// (invariant I6); use this only at event-listener leaves where no caller
24
24
  /// exists.
25
25
  pub fn spawn_owned(tag: &'static str, fut: impl Future<Output = ApiResult<()>> + 'static) {
26
- ApiFuture::spawn(async move {
26
+ ApiFuture::spawn_named(tag, async move {
27
27
  if let Err(e) = fut.await.ignore_view_delete() {
28
28
  tracing::error!("[{}] unhandled task error: {}", tag, e);
29
29
  }