@perspective-dev/viewer 5.2.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 (133) hide show
  1. package/dist/cdn/perspective-viewer.js +2 -2
  2. package/dist/cdn/perspective-viewer.js.map +2 -2
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/monokai.css +1 -1
  9. package/dist/css/phosphor.css +1 -1
  10. package/dist/css/pro-dark.css +1 -1
  11. package/dist/css/pro.css +1 -1
  12. package/dist/css/solarized-dark.css +1 -1
  13. package/dist/css/solarized.css +1 -1
  14. package/dist/css/themes.css +1 -1
  15. package/dist/css/vaporwave.css +1 -1
  16. package/dist/docs/perspective-docs.json +1 -1
  17. package/dist/esm/perspective-viewer.d.ts +1 -0
  18. package/dist/esm/perspective-viewer.inline.js +9 -9
  19. package/dist/esm/perspective-viewer.inline.js.map +2 -2
  20. package/dist/esm/perspective-viewer.js +4 -4
  21. package/dist/esm/perspective-viewer.js.map +2 -2
  22. package/dist/esm/plugin.d.ts +10 -0
  23. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +1 -1
  24. package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
  25. package/dist/esm/ts-rs/ViewerConfig.d.ts +1 -1
  26. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  27. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +13 -3
  28. package/dist/wasm/perspective-viewer.d.ts +43 -36
  29. package/dist/wasm/perspective-viewer.js +194 -136
  30. package/dist/wasm/perspective-viewer.wasm +0 -0
  31. package/dist/wasm/perspective-viewer.wasm.d.ts +19 -17
  32. package/package.json +1 -1
  33. package/src/css/column-settings-panel.css +9 -5
  34. package/src/css/column-style.css +209 -31
  35. package/src/css/dom/select.css +2 -1
  36. package/src/css/form/code-editor.css +4 -4
  37. package/src/rust/agent/tools.rs +48 -17
  38. package/src/rust/components/column_selector/active_column.rs +2 -2
  39. package/src/rust/components/column_selector/add_expression_button.rs +3 -3
  40. package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
  41. package/src/rust/components/column_selector/config_selector.rs +8 -0
  42. package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
  43. package/src/rust/components/column_selector/inactive_column.rs +2 -2
  44. package/src/rust/components/column_selector.rs +48 -4
  45. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
  46. package/src/rust/components/column_settings_sidebar/style_tab.rs +262 -225
  47. package/src/rust/components/column_settings_sidebar.rs +88 -69
  48. package/src/rust/components/editable_header.rs +85 -144
  49. package/src/rust/components/expression_editor.rs +9 -16
  50. package/src/rust/components/form/code_editor.rs +16 -6
  51. package/src/rust/components/form/debug.rs +1 -0
  52. package/src/rust/components/form/mod.rs +3 -1
  53. package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
  54. package/src/rust/components/form/named_value_picker.rs +86 -0
  55. package/src/rust/components/form/palette_selector.rs +440 -0
  56. package/src/rust/components/main_panel/msg.rs +2 -2
  57. package/src/rust/components/main_panel/reconcile.rs +4 -4
  58. package/src/rust/components/plugin_tab.rs +4 -28
  59. package/src/rust/components/settings_panel.rs +25 -20
  60. package/src/rust/components/viewer/msg.rs +46 -7
  61. package/src/rust/components/viewer/panels.rs +11 -9
  62. package/src/rust/components/viewer/render.rs +8 -3
  63. package/src/rust/components/viewer/settings.rs +163 -23
  64. package/src/rust/components/viewer/snapshots.rs +46 -21
  65. package/src/rust/components/viewer/wiring.rs +14 -25
  66. package/src/rust/components/viewer.rs +12 -8
  67. package/src/rust/config/column_config_schema.rs +100 -41
  68. package/src/rust/config/css_scale.rs +882 -0
  69. package/src/rust/config/mod.rs +2 -0
  70. package/src/rust/config/options.rs +10 -0
  71. package/src/rust/config/viewer_config.rs +49 -55
  72. package/src/rust/config/workspace_config.rs +132 -5
  73. package/src/rust/custom_elements/viewer.rs +214 -155
  74. package/src/rust/custom_events.rs +14 -1
  75. package/src/rust/js/regular_layout.rs +3 -3
  76. package/src/rust/lib.rs +2 -0
  77. package/src/rust/presentation/column_locator.rs +19 -5
  78. package/src/rust/presentation.rs +192 -36
  79. package/src/rust/queries/column_locator.rs +74 -44
  80. package/src/rust/queries/get_viewer_config.rs +0 -3
  81. package/src/rust/queries/mod.rs +2 -0
  82. package/src/rust/queries/palette_set.rs +82 -0
  83. package/src/rust/queries/plugin_column_styles.rs +6 -2
  84. package/src/rust/renderer/dispatch.rs +16 -4
  85. package/src/rust/renderer/plugin_config.rs +309 -47
  86. package/src/rust/renderer.rs +15 -42
  87. package/src/rust/session/column_defaults_update.rs +15 -8
  88. package/src/rust/session/metadata.rs +5 -1
  89. package/src/rust/session/props.rs +1 -0
  90. package/src/rust/session.rs +9 -6
  91. package/src/rust/tasks/create_panel.rs +44 -57
  92. package/src/rust/tasks/edit_expression.rs +19 -23
  93. package/src/rust/tasks/edit_window.rs +8 -10
  94. package/src/rust/tasks/eject.rs +4 -1
  95. package/src/rust/tasks/pipeline.rs +2 -2
  96. package/src/rust/tasks/presize_panels.rs +44 -5
  97. package/src/rust/tasks/reset_all.rs +4 -4
  98. package/src/rust/tasks/restore_and_render.rs +17 -44
  99. package/src/rust/tasks/restore_panel.rs +11 -13
  100. package/src/rust/tasks/set_edit_mode.rs +3 -1
  101. package/src/rust/tasks/sync_update_panels.rs +11 -1
  102. package/src/rust/tasks/update_theme.rs +23 -18
  103. package/src/rust/utils/css_vars.rs +26 -0
  104. package/src/rust/utils/mod.rs +2 -0
  105. package/src/rust/utils/spawn.rs +1 -1
  106. package/src/rust/workspace.rs +435 -272
  107. package/src/svg/lock.svg +7 -0
  108. package/src/themes/botanical.css +10 -0
  109. package/src/themes/defaults.css +12 -0
  110. package/src/themes/dracula.css +17 -0
  111. package/src/themes/gruvbox-dark.css +20 -0
  112. package/src/themes/gruvbox.css +10 -0
  113. package/src/themes/icons.css +1 -0
  114. package/src/themes/monokai.css +22 -0
  115. package/src/themes/phosphor.css +5 -0
  116. package/src/themes/pro-dark.css +10 -0
  117. package/src/themes/solarized-dark.css +7 -0
  118. package/src/themes/solarized.css +7 -0
  119. package/src/themes/vaporwave.css +20 -0
  120. package/src/ts/perspective-viewer.ts +1 -0
  121. package/src/ts/plugin.ts +14 -0
  122. package/src/ts/ts-rs/PanelViewerConfig.ts +1 -1
  123. package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
  124. package/src/ts/ts-rs/ViewerConfig.ts +1 -1
  125. package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
  126. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +11 -3
  127. package/src/rust/components/form/color_range_selector.rs +0 -137
  128. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline0.js +0 -0
  129. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline1.js +0 -0
  130. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline2.js +0 -0
  131. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline3.js +0 -0
  132. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline4.js +0 -0
  133. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline5.js +0 -0
@@ -15,6 +15,7 @@
15
15
 
16
16
  mod column_config_schema;
17
17
  mod columns_config;
18
+ mod css_scale;
18
19
  mod datetime_column_style;
19
20
  mod export_method;
20
21
  mod kvpair;
@@ -29,6 +30,7 @@ mod workspace_config;
29
30
 
30
31
  pub use column_config_schema::*;
31
32
  pub use columns_config::*;
33
+ pub use css_scale::*;
32
34
  pub use datetime_column_style::*;
33
35
  pub use export_method::*;
34
36
  pub use number_series_style::*;
@@ -87,3 +87,13 @@ pub struct GetClientOptions {
87
87
  #[ts(optional)]
88
88
  pub panel: Option<String>,
89
89
  }
90
+
91
+ /// Options for the `saveWorkspace()` method.
92
+ #[derive(Deserialize, Default, TS)]
93
+ pub struct SaveWorkspaceOptions {
94
+ /// When `true`, the emitted `palette` is the full set the element
95
+ /// holds rather than only the values the panels reference.
96
+ #[serde(default)]
97
+ #[ts(optional)]
98
+ pub full_palette: Option<bool>,
99
+ }
@@ -24,7 +24,7 @@ use crate::renderer::ColumnConfigMap;
24
24
 
25
25
  /// The state of an entire `custom_elements::PerspectiveViewerElement` component
26
26
  /// and its `Plugin`: the element-level `settings` flag plus the per-panel
27
- /// [`PanelViewerConfig`]. The split exists so the whole-element config format
27
+ /// [`PanelViewerConfig`]. The split exists so the workspace config format
28
28
  /// can serialize panel entries *without* a `settings` key (it is element-level
29
29
  /// state there, carried by the top-level `active` field instead), while the
30
30
  /// single-panel format flattens back to the legacy shape.
@@ -39,7 +39,7 @@ pub struct ViewerConfig<V: TS = String> {
39
39
 
40
40
  /// The per-panel state of a [`ViewerConfig`] — everything except the
41
41
  /// element-level `settings` flag. This is the `panels` entry type of the
42
- /// whole-element config format.
42
+ /// workspace config format.
43
43
  #[derive(Debug, Default, Serialize, PartialEq, TS)]
44
44
  pub struct PanelViewerConfig<V: TS = String> {
45
45
  /// The `@perspective-dev/viewer` version that wrote this config,
@@ -315,15 +315,53 @@ impl From<ViewerConfigInitial> for ViewerConfigUpdate {
315
315
  }
316
316
  }
317
317
 
318
- // There is deliberately NO `TryFrom<ViewerConfigUpdate>` here. Requiring a
319
- // `table` is a property of the CREATION ENTRY POINTS — `addPanel`'s
320
- // argument type, `WorkspaceConfigUpdate::panels`, and the agent's
321
- // `add_panel` decode — each of which already has a `ViewerConfigInitial`
322
- // in hand. An update→initial conversion exists only to let a route holding
323
- // a PATCH pretend it is creating from scratch, which is how `restore`'s
324
- // upsert acquired the gate and started rejecting the table-less
325
- // restore-then-`load` contract. Its absence is what keeps that from
326
- // recurring: `create_panel` takes an update, so no caller needs one.
318
+ /// The rejection every panel-creating route without a `table` resolves to.
319
+ pub const CREATE_REQUIRES_TABLE: &str = "Cannot create a panel without a `table` — `load()` a \
320
+ `Client` and include `table` in the config, or use \
321
+ `addPanel()`";
322
+
323
+ fn down<T: Clone>(value: OptionalUpdate<T>) -> Option<T> {
324
+ match value {
325
+ OptionalUpdate::Update(value) => Some(value),
326
+ OptionalUpdate::Missing | OptionalUpdate::SetDefault => None,
327
+ }
328
+ }
329
+
330
+ impl TryFrom<ViewerConfigUpdate> for ViewerConfigInitial {
331
+ type Error = ApiError;
332
+
333
+ fn try_from(value: ViewerConfigUpdate) -> Result<Self, Self::Error> {
334
+ // Exhaustive (no `..`) on purpose — the same drift alarm as the
335
+ // `From<ViewerConfigInitial>` impl above. `settings` is element-level
336
+ // state, stripped by `restore()` before resolution; discarded here.
337
+ let ViewerConfigUpdate {
338
+ version,
339
+ plugin,
340
+ title,
341
+ table,
342
+ theme,
343
+ settings: _settings,
344
+ plugin_config,
345
+ columns_config,
346
+ view_config,
347
+ } = value;
348
+
349
+ let OptionalUpdate::Update(table) = table else {
350
+ return Err(ApiError::new(CREATE_REQUIRES_TABLE));
351
+ };
352
+
353
+ Ok(Self {
354
+ table,
355
+ version: down(version),
356
+ plugin: down(plugin),
357
+ title: down(title),
358
+ theme: down(theme),
359
+ plugin_config: down(plugin_config),
360
+ columns_config: down(columns_config),
361
+ view_config,
362
+ })
363
+ }
364
+ }
327
365
 
328
366
  impl std::fmt::Display for ViewerConfigUpdate {
329
367
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -360,50 +398,6 @@ pub type VersionUpdate = OptionalUpdate<String>;
360
398
  pub type ColumnConfigUpdate = OptionalUpdate<ColumnConfigMap>;
361
399
  pub type PluginConfigUpdate = OptionalUpdate<serde_json::Map<String, Value>>;
362
400
 
363
- #[cfg(test)]
364
- mod tests {
365
- use super::*;
366
-
367
- #[test]
368
- fn initial_requires_a_table() {
369
- let json = serde_json::json!({ "group_by": ["State"] });
370
- let err = serde_json::from_value::<ViewerConfigInitial>(json).unwrap_err();
371
- assert!(format!("{err}").contains("table"));
372
- }
373
-
374
- /// The creation → patch direction is the only one that exists (see the
375
- /// note above the `From` impl); a `table` becomes a concrete update and
376
- /// the element-level `settings` is not carried.
377
- #[test]
378
- fn initial_widens_to_an_update() {
379
- let json = serde_json::json!({
380
- "table": "superstore",
381
- "plugin": "Datagrid",
382
- "group_by": ["State"],
383
- });
384
-
385
- let initial: ViewerConfigInitial = serde_json::from_value(json).unwrap();
386
- let update = ViewerConfigUpdate::from(initial);
387
- assert!(matches!(&update.table, OptionalUpdate::Update(x) if x == "superstore"));
388
- assert!(matches!(&update.settings, OptionalUpdate::Missing));
389
- assert!(matches!(&update.plugin, OptionalUpdate::Update(x) if x == "Datagrid"));
390
- assert_eq!(
391
- update.view_config.group_by.as_deref(),
392
- Some(&["State".to_owned()][..])
393
- );
394
- }
395
-
396
- /// A table-less patch is a legitimate creation input now that
397
- /// `create_panel` takes an update — the deferred panel a `load()`
398
- /// binds. Nothing in this module may reject it.
399
- #[test]
400
- fn a_table_less_update_is_representable() {
401
- let json = serde_json::json!({ "group_by": ["State"] });
402
- let update: ViewerConfigUpdate = serde_json::from_value(json).unwrap();
403
- assert!(matches!(&update.table, OptionalUpdate::Missing));
404
- }
405
- }
406
-
407
401
  /// Handles `{}` when included as a field with `#[serde(default)]`.
408
402
  impl<T: Clone> Default for OptionalUpdate<T> {
409
403
  fn default() -> Self {
@@ -14,9 +14,9 @@ use std::collections::BTreeMap;
14
14
 
15
15
  use perspective_client::config::Filter;
16
16
 
17
- use crate::config::{PanelViewerConfig, ViewerConfigInitial};
17
+ use crate::config::{CssKind, PanelViewerConfig, ViewerConfigInitial};
18
18
 
19
- /// The whole-element config format (`{version, active?, layout, panels}`) —
19
+ /// The workspace config format (`{version, active?, layout, panels}`) —
20
20
  /// the multi-panel counterpart of the single-panel [`ViewerConfig`] — as
21
21
  /// emitted by [`PerspectiveViewerElement::save`].
22
22
  ///
@@ -56,9 +56,17 @@ pub struct WorkspaceConfig {
56
56
  #[ts(as = "Option<_>")]
57
57
  #[ts(optional)]
58
58
  pub masters: Vec<String>,
59
+
60
+ /// Named color-scale definitions shared by every panel: CSS custom
61
+ /// property name (`--psp-user--<kind>-<name>`) → canonical CSS
62
+ /// value.
63
+ #[serde(skip_serializing_if = "BTreeMap::is_empty")]
64
+ #[ts(as = "Option<_>")]
65
+ #[ts(optional)]
66
+ pub palette: BTreeMap<String, String>,
59
67
  }
60
68
 
61
- /// The parse target of a whole-element config in
69
+ /// The parse target of a workspace config in
62
70
  /// [`PerspectiveViewerElement::restoreWorkspace`]. Mirrors
63
71
  /// [`WorkspaceConfig`], but `panels` entries are [`ViewerConfigInitial`]s —
64
72
  /// every entry creates a NEW panel, so `table` is required by type (a
@@ -79,15 +87,134 @@ pub struct WorkspaceConfigUpdate {
79
87
  /// The element-level global (master/detail cross-) filters to re-apply as
80
88
  /// a transient overlay on every DETAIL panel. Restored as one
81
89
  /// unattributed bucket: the next selection on any master replaces it.
90
+ /// `Option` so an explicit `undefined` property deserializes as
91
+ /// `None` like an absent key (also `masters` / `palette` below).
82
92
  #[serde(default)]
83
93
  #[ts(as = "Option<_>")]
84
94
  #[ts(optional)]
85
- pub global_filters: Vec<Filter>,
95
+ pub global_filters: Option<Vec<Filter>>,
86
96
 
87
97
  /// The master (filter-source) panels, by saved `panels` key. An id not in
88
98
  /// `panels` warns and is dropped.
89
99
  #[serde(default)]
90
100
  #[ts(as = "Option<_>")]
91
101
  #[ts(optional)]
92
- pub masters: Vec<String>,
102
+ pub masters: Option<Vec<String>>,
103
+
104
+ /// Named color-scale definitions to apply to the host (see
105
+ /// [`WorkspaceConfig::palette`]), replacing any previously restored
106
+ /// palette.
107
+ #[serde(default)]
108
+ #[ts(as = "Option<_>")]
109
+ #[ts(optional)]
110
+ pub palette: Option<BTreeMap<String, String>>,
111
+ }
112
+
113
+ /// Validate a restored palette map: each key's `--psp-user--<kind>-`
114
+ /// prefix selects the reader that canonicalizes its value.
115
+ pub fn validate_palette(
116
+ palette: BTreeMap<String, String>,
117
+ ) -> Result<BTreeMap<String, String>, String> {
118
+ palette
119
+ .into_iter()
120
+ .map(|(name, value)| {
121
+ let kind = CssKind::of_var(&name).ok_or_else(|| {
122
+ format!(
123
+ "`palette` key `{name}` must start with `--psp-user--gradient-`, \
124
+ `--psp-user--palette-` or `--psp-user--color-`"
125
+ )
126
+ })?;
127
+
128
+ let canonical = kind
129
+ .canonicalize(&value)
130
+ .map_err(|error| format!("`palette[\"{name}\"]`: {error}"))?;
131
+
132
+ Ok((name, canonical))
133
+ })
134
+ .collect()
135
+ }
136
+
137
+ #[cfg(test)]
138
+ mod tests {
139
+ use serde_json::json;
140
+
141
+ use super::*;
142
+
143
+ #[test]
144
+ fn palette_serializes_only_when_present() {
145
+ let config = WorkspaceConfig {
146
+ version: "x".to_owned(),
147
+ active: None,
148
+ layout: None,
149
+ panels: BTreeMap::new(),
150
+ global_filters: vec![],
151
+ masters: vec![],
152
+ palette: BTreeMap::new(),
153
+ };
154
+
155
+ assert_eq!(
156
+ serde_json::to_value(&config).unwrap(),
157
+ json!({ "version": "x", "layout": null, "panels": {} })
158
+ );
159
+
160
+ let mut palette = BTreeMap::new();
161
+ palette.insert("--psp-user--color-hot".to_owned(), "#ff0000".to_owned());
162
+
163
+ let config = WorkspaceConfig { palette, ..config };
164
+ assert_eq!(
165
+ serde_json::to_value(&config).unwrap(),
166
+ json!({
167
+ "version": "x",
168
+ "layout": null,
169
+ "panels": {},
170
+ "palette": { "--psp-user--color-hot": "#ff0000" },
171
+ })
172
+ );
173
+ }
174
+
175
+ #[test]
176
+ fn update_palette_defaults_empty_and_validates_by_prefix() {
177
+ let update: WorkspaceConfigUpdate =
178
+ serde_json::from_value(json!({ "panels": {} })).unwrap();
179
+ assert!(update.palette.is_none());
180
+
181
+ let update: WorkspaceConfigUpdate = serde_json::from_value(json!({
182
+ "panels": {},
183
+ "palette": {
184
+ "--psp-user--gradient-1": "linear-gradient(#000, #fff)",
185
+ "--psp-user--palette-warm": "linear-gradient(90deg, RGB(255,0,0), #ff0)",
186
+ "--psp-user--color-hot": "#F00",
187
+ },
188
+ }))
189
+ .unwrap();
190
+
191
+ let valid = validate_palette(update.palette.unwrap()).unwrap();
192
+ assert_eq!(
193
+ valid.get("--psp-user--gradient-1").unwrap(),
194
+ "linear-gradient(to right, #000000 0%, #ffffff 100%)"
195
+ );
196
+ assert_eq!(
197
+ valid.get("--psp-user--palette-warm").unwrap(),
198
+ "linear-gradient(to right, #ff0000, #ffff00)"
199
+ );
200
+ assert_eq!(valid.get("--psp-user--color-hot").unwrap(), "#ff0000");
201
+
202
+ let bad = |name: &str, value: &str| {
203
+ let mut map = BTreeMap::new();
204
+ map.insert(name.to_owned(), value.to_owned());
205
+ validate_palette(map).unwrap_err()
206
+ };
207
+
208
+ assert!(bad("--psp-user--other-1", "#ff0000").contains("--psp-user--other-1"));
209
+ assert!(bad("--psp-charts--gradient", "#ff0000").contains("must start with"));
210
+ assert!(
211
+ bad(
212
+ "--psp-user--palette-1",
213
+ "linear-gradient(#000 0%, #fff 100%)"
214
+ )
215
+ .contains("--psp-user--palette-1")
216
+ );
217
+ assert!(bad("--psp-user--gradient-1", "#ff0000").contains("linear-gradient"));
218
+ assert!(bad("--psp-user--color-1", "red").contains("red"));
219
+ }
93
220
  }