@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
@@ -54,7 +54,8 @@ pub async fn get_viewer_config(
54
54
  let table = session
55
55
  .get_table()
56
56
  .map(|x| x.get_name().to_owned())
57
- .or_else(|| session.pending_table());
57
+ .or_else(|| session.pending_table())
58
+ .ok_or_else(|| ApiError::from("Panel has no `table`"))?;
58
59
  let columns_config = renderer.all_columns_configs();
59
60
  Ok(ViewerConfig {
60
61
  settings,
@@ -29,6 +29,7 @@ mod exports;
29
29
  mod fetch_column_stats;
30
30
  mod get_viewer_config;
31
31
  mod is_invalid_drop;
32
+ mod palette_set;
32
33
  mod plugin_column_styles;
33
34
  mod validate_expression;
34
35
 
@@ -40,5 +41,6 @@ pub use self::exports::*;
40
41
  pub use self::fetch_column_stats::*;
41
42
  pub use self::get_viewer_config::*;
42
43
  pub use self::is_invalid_drop::*;
44
+ pub use self::palette_set::*;
43
45
  pub use self::plugin_column_styles::*;
44
46
  pub use self::validate_expression::*;
@@ -0,0 +1,82 @@
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 std::collections::BTreeMap;
14
+
15
+ use crate::config::{CssKind, CssLiteralUse, NamedValue, assign_palette_names};
16
+ use crate::presentation::Presentation;
17
+ use crate::workspace::{PanelId, Workspace};
18
+
19
+ /// Every CSS literal in use across the workspace, as `(panel id, use)`,
20
+ /// in a deterministic order.
21
+ pub fn styles_in_use(workspace: &Workspace) -> Vec<(PanelId, CssLiteralUse)> {
22
+ let mut ids = workspace.panel_ids();
23
+ ids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
24
+ let mut out = vec![];
25
+ for id in ids {
26
+ let Some(panel) = workspace.panel(&id) else {
27
+ continue;
28
+ };
29
+
30
+ let uses = {
31
+ let view_config = panel.session.get_view_config();
32
+ panel
33
+ .renderer
34
+ .css_literals_in_use(&view_config, &panel.session)
35
+ };
36
+
37
+ out.extend(uses.into_iter().map(|x| (id.clone(), x)));
38
+ }
39
+
40
+ out
41
+ }
42
+
43
+ /// The workspace palette set: the restored palette ∪ every literal in
44
+ /// use, named stably (see [`crate::config::assign_palette_names`]).
45
+ pub fn palette_set(workspace: &Workspace, presentation: &Presentation) -> BTreeMap<String, String> {
46
+ let host: Vec<NamedValue> = CssKind::ALL
47
+ .into_iter()
48
+ .flat_map(|kind| presentation.host_named_values(kind))
49
+ .collect();
50
+
51
+ let in_use: Vec<(CssKind, String)> = styles_in_use(workspace)
52
+ .into_iter()
53
+ .map(|(_, x)| (x.kind, x.literal))
54
+ .collect();
55
+
56
+ assign_palette_names(&presentation.palette(), &host, &in_use, &|name| {
57
+ presentation.resolve_css_var(name).is_some()
58
+ })
59
+ }
60
+
61
+ /// The named values of `kind` a picker offers: the palette set's
62
+ /// entries, then any theme/page-authored host entries not already in
63
+ /// the set by name.
64
+ pub fn named_values(
65
+ workspace: &Workspace,
66
+ presentation: &Presentation,
67
+ kind: CssKind,
68
+ ) -> Vec<NamedValue> {
69
+ let mut out: Vec<NamedValue> = palette_set(workspace, presentation)
70
+ .into_iter()
71
+ .filter(|(name, _)| name.starts_with(kind.var_prefix()))
72
+ .map(|(name, value)| NamedValue { name, value })
73
+ .collect();
74
+
75
+ for entry in presentation.host_named_values(kind) {
76
+ if !out.iter().any(|x| x.name == entry.name) {
77
+ out.push(entry);
78
+ }
79
+ }
80
+
81
+ out
82
+ }
@@ -41,7 +41,9 @@ pub fn get_plugin_config_schema(
41
41
  let view_config_js =
42
42
  wasm_bindgen::JsValue::from_serde_ext(view_config).unwrap_or(wasm_bindgen::JsValue::NULL);
43
43
  let raw = plugin._plugin_config_schema(&view_config_js)?;
44
- serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
44
+ serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
45
+ .map(|schema| schema.canonicalize_defaults())
46
+ .map_err(|e| e.into())
45
47
  }
46
48
 
47
49
  /// Queries the active plugin for the per-column [`ColumnConfigSchema`].
@@ -97,5 +99,7 @@ pub fn get_column_config_schema(
97
99
  &stats_js,
98
100
  )?;
99
101
 
100
- serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
102
+ serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
103
+ .map(|schema| schema.canonicalize_defaults())
104
+ .map_err(|e| e.into())
101
105
  }
@@ -49,7 +49,7 @@ impl Renderer {
49
49
  let active_plugin = self.active_plugin();
50
50
  if let Some(plugin) = plugin.or(active_plugin.as_ref()) {
51
51
  let theme_elem = plugin.unchecked_ref::<HtmlElement>();
52
- match self.effective_theme() {
52
+ match self.theme() {
53
53
  Some(theme)
54
54
  if theme_elem.get_attribute("theme").as_deref() != Some(theme.as_str()) =>
55
55
  {
@@ -73,7 +73,7 @@ impl Renderer {
73
73
  let _pin = self.pin_context(&guard, ctx.clone());
74
74
  let plugin = self.ensure_plugin_selected()?;
75
75
  let meta = self.metadata();
76
- let stamped_theme = self.effective_theme();
76
+ let stamped_theme = self.theme();
77
77
  self.stamp_theme(Some(&plugin));
78
78
  self.stamp_active(&plugin);
79
79
  plugin.restyle();
@@ -134,7 +134,19 @@ impl Renderer {
134
134
 
135
135
  pub async fn resize(&self) -> ApiResult<()> {
136
136
  self.0.geometry_cmd.set(Some(GeometryCmd::Measure));
137
- self.geometry_task().await.map(|_| ())
137
+
138
+ // This caller queued a `Measure`, but the parked run it resolves
139
+ // through executes the LATEST `geometry_cmd` — which a concurrent
140
+ // presize sweep may have swapped to a `Presize` whose sweep
141
+ // coalesced (and so received `None`). Any present closure returned
142
+ // here is therefore held by no one else: invoke it, or the
143
+ // transport's present-hold leaks and the plot freezes on its
144
+ // last-blitted frame.
145
+ if let Some(present) = self.geometry_task().await? {
146
+ present.call0(&JsValue::NULL)?;
147
+ }
148
+
149
+ Ok(())
138
150
  }
139
151
 
140
152
  /// Public one-shot resize to explicit dimensions (`viewer.resize({
@@ -416,7 +428,7 @@ impl Renderer {
416
428
  let viewer_elem = self.0.borrow().viewer_elem.clone();
417
429
  let slot = self.slot_name();
418
430
  let first_paint = !self.0.has_drawn.get();
419
- let stamped_theme = self.effective_theme();
431
+ let stamped_theme = self.theme();
420
432
  self.stamp_active(&plugin);
421
433
  self.stamp_theme(Some(&plugin));
422
434
  if self.needs_restyle() {
@@ -18,7 +18,7 @@ use std::collections::HashMap;
18
18
 
19
19
  use futures::future::join_all;
20
20
  use perspective_client::config::ViewConfig;
21
- use perspective_js::utils::{ApiResult, JsValueSerdeExt};
21
+ use perspective_js::utils::{ApiError, ApiResult, JsValueSerdeExt};
22
22
  use serde_json::Value;
23
23
  use wasm_bindgen::prelude::*;
24
24
 
@@ -63,6 +63,50 @@ impl Renderer {
63
63
  .unwrap_or_default()
64
64
  }
65
65
 
66
+ fn resolve_css_refs_in(&self, column: &str, entry: &mut serde_json::Map<String, Value>) {
67
+ let host = self.borrow().viewer_elem.clone();
68
+ let lookup = |name: &str| crate::utils::read_custom_property(&host, name);
69
+ for (key, name) in resolve_css_refs(entry, &lookup) {
70
+ tracing::warn!(
71
+ "Dropping `columns_config[\"{column}\"].{key}`: `var({name})` is undefined or not \
72
+ a valid value of its kind"
73
+ );
74
+ }
75
+ }
76
+
77
+ /// Every CSS literal stored in the active plugin's per-column bucket
78
+ /// with its schema kind, sorted by `(column, key)`.
79
+ pub fn css_literals_in_use(
80
+ &self,
81
+ view_config: &ViewConfig,
82
+ session: &Session,
83
+ ) -> Vec<CssLiteralUse> {
84
+ let mut out = vec![];
85
+ for (column, entry) in self.all_columns_configs() {
86
+ let Ok(schema) =
87
+ self.query_column_config_schema(view_config, session, &column, Some(&entry))
88
+ else {
89
+ continue;
90
+ };
91
+
92
+ for (key, value) in &entry {
93
+ if let (Some(kind), Some(literal)) = (schema.css_kind_of(key), value.as_str())
94
+ && parse_var_ref(literal).is_none()
95
+ {
96
+ out.push(CssLiteralUse {
97
+ column: column.clone(),
98
+ key: key.clone(),
99
+ kind,
100
+ literal: literal.to_owned(),
101
+ });
102
+ }
103
+ }
104
+ }
105
+
106
+ out.sort_by(|a, b| (&a.column, &a.key).cmp(&(&b.column, &b.key)));
107
+ out
108
+ }
109
+
66
110
  /// Restore-prep snapshot: like [`Self::all_columns_configs`], but
67
111
  /// for each column also materializes any `ControlSpec::Number`
68
112
  /// fields the schema declares with `include: true` that aren't
@@ -173,15 +217,15 @@ impl Renderer {
173
217
  .cloned()
174
218
  }
175
219
 
176
- /// Wholesale update the active plugin's per-column config map
220
+ /// Wholesale update the active plugin's per-column config map.
177
221
  pub fn update_columns_configs(
178
222
  &self,
179
223
  view_config: &ViewConfig,
180
224
  session: &Session,
181
225
  update: ColumnConfigUpdate,
182
- ) -> bool {
226
+ ) -> ApiResult<bool> {
183
227
  let Some(n) = self.active_plugin_name() else {
184
- return false;
228
+ return Ok(false);
185
229
  };
186
230
 
187
231
  match update {
@@ -190,24 +234,31 @@ impl Renderer {
190
234
  let bucket = st.plugin_states.entry(n).or_default();
191
235
  let was_nonempty = !bucket.columns.is_empty();
192
236
  bucket.columns.clear();
193
- was_nonempty
237
+ Ok(was_nonempty)
194
238
  },
195
- OptionalUpdate::Missing => false,
239
+ OptionalUpdate::Missing => Ok(false),
196
240
  OptionalUpdate::Update(map) => {
197
- let stripped: Vec<(String, serde_json::Map<String, serde_json::Value>)> = map
198
- .into_iter()
199
- .map(|(col, mut cfg)| {
200
- if let Ok(schema) =
201
- self.query_column_config_schema(view_config, session, &col, Some(&cfg))
202
- {
203
- let active = schema.active_keys();
204
- cfg.retain(|k, _| active.contains(k));
205
- strip_default_values(&schema, &mut cfg);
241
+ let mut stripped: Vec<(String, serde_json::Map<String, serde_json::Value>)> =
242
+ Vec::with_capacity(map.len());
243
+ for (col, mut cfg) in map {
244
+ if let Ok(schema) =
245
+ self.query_column_config_schema(view_config, session, &col, Some(&cfg))
246
+ {
247
+ let active = schema.active_keys();
248
+ cfg.retain(|k, _| active.contains(k));
249
+ let errors = normalize_css_values(&schema, &mut cfg);
250
+ if let Some((key, error)) = errors.first() {
251
+ return Err(ApiError::from(JsValue::from_str(&format!(
252
+ "Invalid `columns_config[\"{col}\"].{key}`: {error}"
253
+ ))));
206
254
  }
207
255
 
208
- (col, cfg)
209
- })
210
- .collect();
256
+ self.resolve_css_refs_in(&col, &mut cfg);
257
+ strip_default_values(&schema, &mut cfg);
258
+ }
259
+
260
+ stripped.push((col, cfg));
261
+ }
211
262
 
212
263
  let mut st = self.borrow_mut();
213
264
  let bucket = st.plugin_states.entry(n).or_default();
@@ -226,7 +277,7 @@ impl Renderer {
226
277
  }
227
278
  }
228
279
 
229
- changed
280
+ Ok(changed)
230
281
  },
231
282
  }
232
283
  }
@@ -253,22 +304,45 @@ impl Renderer {
253
304
  &column_name,
254
305
  current_value.as_ref(),
255
306
  ) {
307
+ for (key, error) in normalize_css_values(&schema, &mut update.value) {
308
+ tracing::error!("Dropping `{column_name}`.`{key}`: {error}");
309
+ }
310
+
311
+ self.resolve_css_refs_in(&column_name, &mut update.value);
256
312
  strip_default_values(&schema, &mut update.value);
257
313
  }
258
314
 
315
+ let next = {
316
+ let mut st = self.borrow_mut();
317
+ let bucket = st.plugin_states.entry(n.clone()).or_default();
318
+ let entry = bucket.columns.entry(column_name.clone()).or_default();
319
+ for k in &update.keys {
320
+ entry.remove(k);
321
+ }
322
+ for (k, v) in update.value {
323
+ if update.keys.contains(&k) {
324
+ entry.insert(k, v);
325
+ }
326
+ }
327
+
328
+ entry.clone()
329
+ };
330
+
331
+ let active = self
332
+ .query_column_config_schema(view_config, session, &column_name, Some(&next))
333
+ .ok()
334
+ .map(|schema| schema.active_keys());
335
+
259
336
  let mut st = self.borrow_mut();
260
337
  let bucket = st.plugin_states.entry(n).or_default();
261
- let entry = bucket.columns.entry(column_name.clone()).or_default();
262
- for k in &update.keys {
263
- entry.remove(k);
264
- }
265
- for (k, v) in update.value {
266
- if update.keys.contains(&k) {
267
- entry.insert(k, v);
338
+ if let Some(entry) = bucket.columns.get_mut(&column_name) {
339
+ if let Some(active) = &active {
340
+ entry.retain(|k, _| active.contains(k));
341
+ }
342
+
343
+ if entry.is_empty() {
344
+ bucket.columns.remove(&column_name);
268
345
  }
269
- }
270
- if entry.is_empty() {
271
- bucket.columns.remove(&column_name);
272
346
  }
273
347
  }
274
348
 
@@ -305,7 +379,9 @@ impl Renderer {
305
379
  let plugin = self.ensure_plugin_selected()?;
306
380
  let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
307
381
  let raw = plugin._plugin_config_schema(&view_config_js)?;
308
- serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
382
+ serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
383
+ .map(|schema| schema.canonicalize_defaults())
384
+ .map_err(|e| e.into())
309
385
  }
310
386
 
311
387
  /// Per-column counterpart of [`query_plugin_config_schema`]. Used by
@@ -352,7 +428,9 @@ impl Renderer {
352
428
  &stats_js,
353
429
  )?;
354
430
 
355
- serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
431
+ serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
432
+ .map(|schema| schema.canonicalize_defaults())
433
+ .map_err(|e| e.into())
356
434
  }
357
435
 
358
436
  /// Wholesale update the active plugin's plugin-level config map.
@@ -365,9 +443,9 @@ impl Renderer {
365
443
  &self,
366
444
  view_config: &ViewConfig,
367
445
  update: PluginConfigUpdate,
368
- ) -> bool {
446
+ ) -> ApiResult<bool> {
369
447
  let Some(n) = self.active_plugin_name() else {
370
- return false;
448
+ return Ok(false);
371
449
  };
372
450
 
373
451
  let schema = self.query_plugin_config_schema(view_config).ok();
@@ -377,14 +455,21 @@ impl Renderer {
377
455
  OptionalUpdate::SetDefault => {
378
456
  let changed = !bucket.plugin.is_empty();
379
457
  bucket.plugin.clear();
380
- changed
458
+ Ok(changed)
381
459
  },
382
- OptionalUpdate::Missing => false,
460
+ OptionalUpdate::Missing => Ok(false),
383
461
  OptionalUpdate::Update(mut map) => {
384
462
  let mut changed = false;
385
463
  if let Some(s) = &schema {
386
464
  let active = s.active_keys();
387
465
  map.retain(|k, _| active.contains(k));
466
+ let errors = normalize_css_values(s, &mut map);
467
+ if let Some((key, error)) = errors.first() {
468
+ return Err(ApiError::from(JsValue::from_str(&format!(
469
+ "Invalid `plugin_config.{key}`: {error}"
470
+ ))));
471
+ }
472
+
388
473
  map.retain(|key, value| {
389
474
  let is_default = s
390
475
  .fields
@@ -408,7 +493,7 @@ impl Renderer {
408
493
  }
409
494
  }
410
495
 
411
- changed
496
+ Ok(changed)
412
497
  },
413
498
  }
414
499
  }
@@ -424,6 +509,10 @@ impl Renderer {
424
509
  };
425
510
 
426
511
  if let Ok(schema) = self.query_plugin_config_schema(view_config) {
512
+ for (key, error) in normalize_css_values(&schema, &mut update.value) {
513
+ tracing::error!("Dropping `plugin_config`.`{key}`: {error}");
514
+ }
515
+
427
516
  strip_default_values(&schema, &mut update.value);
428
517
  }
429
518
 
@@ -446,6 +535,34 @@ impl Renderer {
446
535
  }
447
536
  }
448
537
 
538
+ fn normalize_css_values(
539
+ schema: &ColumnConfigSchema,
540
+ map: &mut serde_json::Map<String, serde_json::Value>,
541
+ ) -> Vec<(String, String)> {
542
+ let mut errors = vec![];
543
+ for (key, value) in map.iter_mut() {
544
+ let Some(kind) = schema.css_kind_of(key) else {
545
+ continue;
546
+ };
547
+
548
+ let canonical = match value.as_str() {
549
+ Some(src) => kind.canonicalize(src),
550
+ None => Err(format!("expected a CSS string, got `{value}`")),
551
+ };
552
+
553
+ match canonical {
554
+ Ok(canonical) => *value = Value::String(canonical),
555
+ Err(error) => errors.push((key.clone(), error)),
556
+ }
557
+ }
558
+
559
+ for (key, _) in &errors {
560
+ map.remove(key);
561
+ }
562
+
563
+ errors
564
+ }
565
+
449
566
  fn strip_default_values(
450
567
  schema: &ColumnConfigSchema,
451
568
  map: &mut serde_json::Map<String, serde_json::Value>,
@@ -479,17 +596,162 @@ fn matches_declared_default(spec: &ControlSpec, key: &str, value: &Value) -> boo
479
596
  } if k == key => value.as_str() == Some(default.as_str()),
480
597
  ControlSpec::Color {
481
598
  key: k, default, ..
482
- } if k == key => value.as_str() == Some(default.as_str()),
483
- ControlSpec::ColorRange {
484
- key_pos,
485
- default_pos,
486
- ..
487
- } if key_pos == key => value.as_str() == Some(default_pos.as_str()),
488
- ControlSpec::ColorRange {
489
- key_neg,
490
- default_neg,
491
- ..
492
- } if key_neg == key => value.as_str() == Some(default_neg.as_str()),
599
+ } if k == key => css_matches(CssKind::Color, value, default),
600
+ ControlSpec::Palette {
601
+ key: k, default, ..
602
+ } if k == key => css_matches(CssKind::Palette, value, default),
603
+ ControlSpec::GradientStops {
604
+ key: k, default, ..
605
+ } if k == key => css_matches(CssKind::Gradient, value, default),
493
606
  _ => false,
494
607
  }
495
608
  }
609
+
610
+ fn css_matches(kind: CssKind, value: &Value, default: &str) -> bool {
611
+ value
612
+ .as_str()
613
+ .and_then(|src| kind.canonicalize(src).ok())
614
+ .is_some_and(|canonical| canonical == default)
615
+ }
616
+
617
+ #[cfg(test)]
618
+ mod tests {
619
+ use serde_json::json;
620
+
621
+ use super::*;
622
+
623
+ #[test]
624
+ fn palette_default_matches_canonically() {
625
+ let spec = ControlSpec::Palette {
626
+ key: "palette".to_owned(),
627
+ default: "linear-gradient(to right, #0366d6, #ff7f0e)".to_owned(),
628
+ max: None,
629
+ };
630
+
631
+ assert!(matches_declared_default(
632
+ &spec,
633
+ "palette",
634
+ &json!("linear-gradient(to right, #0366d6, #ff7f0e)")
635
+ ));
636
+
637
+ assert!(matches_declared_default(
638
+ &spec,
639
+ "palette",
640
+ &json!("linear-gradient(90deg, RGB(3,102,214), #FF7F0E)")
641
+ ));
642
+
643
+ assert!(!matches_declared_default(
644
+ &spec,
645
+ "palette",
646
+ &json!("linear-gradient(to right, #ff7f0e, #0366d6)")
647
+ ));
648
+
649
+ assert!(!matches_declared_default(
650
+ &spec,
651
+ "palette",
652
+ &json!("var(--psp-user--palette-1)")
653
+ ));
654
+
655
+ assert!(!matches_declared_default(
656
+ &spec,
657
+ "palette",
658
+ &json!(["#0366d6"])
659
+ ));
660
+ assert!(!matches_declared_default(
661
+ &spec,
662
+ "other",
663
+ &json!("linear-gradient(to right, #0366d6, #ff7f0e)")
664
+ ));
665
+ }
666
+
667
+ #[test]
668
+ fn gradient_default_matches_canonical_values() {
669
+ let spec = ControlSpec::GradientStops {
670
+ key: "gradient".to_owned(),
671
+ default: "linear-gradient(to right, #0366d6 0%, #ff7f0e 33.3%)".to_owned(),
672
+ discrete: false,
673
+ };
674
+
675
+ assert!(matches_declared_default(
676
+ &spec,
677
+ "gradient",
678
+ &json!("linear-gradient(#0366d6, #ff7f0e 33.3%)")
679
+ ));
680
+
681
+ assert!(!matches_declared_default(
682
+ &spec,
683
+ "gradient",
684
+ &json!("linear-gradient(to right, #0366d6 0%, #ff7f0e 33.4%)")
685
+ ));
686
+
687
+ let color = ControlSpec::Color {
688
+ key: "color".to_owned(),
689
+ default: "#ff7f0e".to_owned(),
690
+ };
691
+
692
+ assert!(matches_declared_default(&color, "color", &json!("#FF7F0E")));
693
+ assert!(matches_declared_default(
694
+ &color,
695
+ "color",
696
+ &json!("rgb(255, 127, 14)")
697
+ ));
698
+ assert!(!matches_declared_default(
699
+ &color,
700
+ "color",
701
+ &json!("var(--psp-user--color-1)")
702
+ ));
703
+ }
704
+
705
+ #[test]
706
+ fn normalize_css_values_canonicalizes_and_reports() {
707
+ let schema = ColumnConfigSchema {
708
+ fields: vec![
709
+ ControlSpec::Color {
710
+ key: "color".to_owned(),
711
+ default: "#000000".to_owned(),
712
+ },
713
+ ControlSpec::Palette {
714
+ key: "palette".to_owned(),
715
+ default: "linear-gradient(to right, #000000)".to_owned(),
716
+ max: None,
717
+ },
718
+ ControlSpec::GradientStops {
719
+ key: "gradient".to_owned(),
720
+ default: "linear-gradient(to right, #000000 0%, #ffffff 100%)".to_owned(),
721
+ discrete: false,
722
+ },
723
+ ControlSpec::Bool {
724
+ key: "flag".to_owned(),
725
+ default: false,
726
+ },
727
+ ],
728
+ };
729
+
730
+ let mut map = json!({
731
+ "color": "RGB(255,0,0)",
732
+ "palette": "var(--psp-user--palette-warm)",
733
+ "gradient": [{ "color": "#000000", "offset": 0 }],
734
+ "flag": true,
735
+ "foreign": "linear-gradient(red, blue)",
736
+ })
737
+ .as_object()
738
+ .unwrap()
739
+ .clone();
740
+
741
+ let errors = normalize_css_values(&schema, &mut map);
742
+ assert_eq!(errors.len(), 1);
743
+ assert_eq!(errors[0].0, "gradient");
744
+ assert_eq!(
745
+ map,
746
+ json!({
747
+ "color": "#ff0000",
748
+ "palette": "var(--psp-user--palette-warm)",
749
+ "flag": true,
750
+ "foreign": "linear-gradient(red, blue)",
751
+ })
752
+ .as_object()
753
+ .unwrap()
754
+ .clone()
755
+ );
756
+ }
757
+ }