@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
@@ -150,6 +150,10 @@ fn dispatch_config_update(
150
150
  renderer: &Renderer,
151
151
  presentation: &Presentation,
152
152
  ) {
153
+ if session.get_table().is_none() && session.pending_table().is_none() {
154
+ return;
155
+ }
156
+
153
157
  clone!(session, renderer, presentation);
154
158
  let elem = elem.clone();
155
159
  let tracker = session.clone();
@@ -218,7 +222,16 @@ pub fn wire_element_events(
218
222
 
219
223
  let settings_sub = presentation.settings_open_changed.add_listener({
220
224
  clone!(elem, presentation, workspace);
221
- move |open: bool| {
225
+ move |(open, announce): (bool, bool)| {
226
+ // A silent toggle (`restore`/`restoreWorkspace`) is announced
227
+ // by its own view-config commit dispatch — emitting here too
228
+ // would double-fire `perspective-config-update`, the first
229
+ // carrying the intermediate config (see
230
+ // `PresentationHandle::settings_open_changed`).
231
+ if !announce {
232
+ return;
233
+ }
234
+
222
235
  dispatch_event(&elem, "toggle-settings", open).unwrap();
223
236
  if let Some(panel) = workspace.active_panel() {
224
237
  dispatch_config_update(&elem, &panel.session, &panel.renderer, &presentation);
@@ -261,7 +274,36 @@ pub fn wire_element_events(
261
274
  }
262
275
  });
263
276
 
277
+ let layout_sub = workspace.layout_changed().add_listener({
278
+ clone!(elem);
279
+ move |panels: Vec<crate::workspace::PanelId>| {
280
+ let ids = panels
281
+ .iter()
282
+ .map(|id| JsValue::from_str(id.as_str()))
283
+ .collect::<js_sys::Array>();
284
+
285
+ let detail = js_sys::Object::new();
286
+ let _ = js_sys::Reflect::set(&detail, &JsValue::from_str("panels"), &ids);
287
+ dispatch_event(&elem, "layout-update", JsValue::from(detail)).unwrap();
288
+ }
289
+ });
290
+
291
+ let active_panel_sub = workspace.active_changed().add_listener({
292
+ clone!(elem);
293
+ move |active: Option<crate::workspace::PanelId>| {
294
+ let panel = active
295
+ .map(|id| JsValue::from_str(id.as_str()))
296
+ .unwrap_or(JsValue::NULL);
297
+
298
+ let detail = js_sys::Object::new();
299
+ let _ = js_sys::Reflect::set(&detail, &JsValue::from_str("panel"), &panel);
300
+ dispatch_event(&elem, "active-panel-update", JsValue::from(detail)).unwrap();
301
+ }
302
+ });
303
+
264
304
  vec![
305
+ layout_sub,
306
+ active_panel_sub,
265
307
  theme_sub,
266
308
  before_settings_sub,
267
309
  settings_sub,
@@ -235,9 +235,9 @@ impl Layout {
235
235
  }
236
236
 
237
237
  /// Rewrite every panel id through `f`, returning the remapped tree. Ids for
238
- /// which `f` returns `None` are left unchanged. Used by whole-element
239
- /// `restore`, which recreates panels under fresh (collision-free) ids and
240
- /// must point the saved layout tree at them.
238
+ /// which `f` returns `None` are left unchanged. Used by `restoreWorkspace`,
239
+ /// which recreates panels under fresh (collision-free) ids and must point
240
+ /// the saved layout tree at them.
241
241
  pub fn remap(&self, f: &impl Fn(&str) -> Option<String>) -> Layout {
242
242
  match self {
243
243
  Layout::Split(split) => Layout::Split(SplitLayout {
package/src/rust/lib.rs CHANGED
@@ -31,6 +31,8 @@
31
31
  // `SESSION_CONFIG_COHERENCE_PLAN.md`) — deny, don't warn.
32
32
  #![deny(clippy::await_holding_refcell_ref)]
33
33
 
34
+ #[cfg(feature = "llm-agent")]
35
+ mod agent;
34
36
  pub mod components;
35
37
  pub mod config;
36
38
  pub mod custom_elements;
@@ -86,22 +88,35 @@ import type {
86
88
 
87
89
  export type * from "../../src/ts/ts-rs/ViewerConfig.d.ts";
88
90
  export type * from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts";
91
+ export type * from "../../src/ts/ts-rs/ViewerConfigInitial.d.ts";
89
92
  export type * from "../../src/ts/ts-rs/PluginStaticConfig.d.ts";
90
93
  export type * from "../../src/ts/ts-rs/WorkspaceConfig.d.ts";
91
94
  export type * from "../../src/ts/ts-rs/WorkspaceConfigUpdate.d.ts";
92
95
  export type * from "../../src/ts/ts-rs/ExportMethod.d.ts";
93
96
  export type * from "../../src/ts/ts-rs/PanelOptions.d.ts";
97
+ export type * from "../../src/ts/ts-rs/RestoreOptions.d.ts";
98
+ export type * from "../../src/ts/ts-rs/SaveWorkspaceOptions.d.ts";
94
99
  export type * from "../../src/ts/ts-rs/ClientOptions.d.ts";
95
100
  export type * from "../../src/ts/ts-rs/ExportOptions.d.ts";
96
101
  export type * from "../../src/ts/ts-rs/GetTableOptions.d.ts";
97
102
  export type * from "../../src/ts/ts-rs/GetClientOptions.d.ts";
103
+ export type * from "../../src/ts/ts-rs/CustomNumberFormatConfig.d.ts";
104
+ export type * from "../../src/ts/ts-rs/NumberFormatStyle.d.ts";
105
+ export type * from "../../src/ts/ts-rs/Notation.d.ts";
106
+ export type * from "../../src/ts/ts-rs/DatetimeFormatType.d.ts";
107
+ export type * from "../../src/ts/ts-rs/StringColorMode.d.ts";
108
+ export type * from "../../src/ts/ts-rs/DatetimeColorMode.d.ts";
109
+ export type * from "../../src/ts/ts-rs/FormatMode.d.ts";
98
110
  import type {GetTableOptions} from "../../src/ts/ts-rs/GetTableOptions.d.ts";
99
111
  import type {PanelOptions} from "../../src/ts/ts-rs/PanelOptions.d.ts";
112
+ import type {RestoreOptions} from "../../src/ts/ts-rs/RestoreOptions.d.ts";
113
+ import type {SaveWorkspaceOptions} from "../../src/ts/ts-rs/SaveWorkspaceOptions.d.ts";
100
114
  import type {ExportOptions} from "../../src/ts/ts-rs/ExportOptions.d.ts";
101
115
  import type {GetClientOptions} from "../../src/ts/ts-rs/GetClientOptions.d.ts";
102
116
  import type {ClientOptions} from "../../src/ts/ts-rs/ClientOptions.d.ts";
103
117
  import type {ViewerConfig} from "../../src/ts/ts-rs/ViewerConfig.d.ts";
104
118
  import type {ViewerConfigUpdate} from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts";
119
+ import type {ViewerConfigInitial} from "../../src/ts/ts-rs/ViewerConfigInitial.d.ts";
105
120
  import type {WorkspaceConfig} from "../../src/ts/ts-rs/WorkspaceConfig.d.ts";
106
121
  import type {WorkspaceConfigUpdate} from "../../src/ts/ts-rs/WorkspaceConfigUpdate.d.ts";
107
122
  "#;
@@ -66,6 +66,23 @@ impl ColumnLocator {
66
66
  }
67
67
  }
68
68
 
69
+ /// What the column-settings drawer is opened on, as held in presentation
70
+ /// state: a column by name, or the not-yet-saved expression.
71
+ #[derive(Clone, Debug, PartialEq)]
72
+ pub enum ColumnSettingsTarget {
73
+ Column(String),
74
+ NewExpression,
75
+ }
76
+
77
+ impl ColumnSettingsTarget {
78
+ pub fn name(&self) -> Option<&String> {
79
+ match self {
80
+ Self::Column(name) => Some(name),
81
+ Self::NewExpression => None,
82
+ }
83
+ }
84
+ }
85
+
69
86
  #[derive(Debug, Default, Clone, Copy, PartialEq)]
70
87
 
71
88
  pub enum ColumnSettingsTab {
@@ -83,16 +100,13 @@ impl Display for ColumnSettingsTab {
83
100
 
84
101
  #[derive(Clone, Default, Debug, PartialEq)]
85
102
  pub struct OpenColumnSettings {
86
- pub locator: Option<ColumnLocator>,
103
+ pub target: Option<ColumnSettingsTarget>,
87
104
  pub tab: Option<ColumnSettingsTab>,
88
105
  }
89
106
 
90
107
  impl OpenColumnSettings {
91
108
  pub fn name(&self) -> Option<String> {
92
- self.locator
93
- .as_ref()
94
- .and_then(|l| l.name())
95
- .map(|s| s.to_owned())
109
+ self.target.as_ref().and_then(|t| t.name()).cloned()
96
110
  }
97
111
  }
98
112
 
@@ -15,8 +15,8 @@ pub mod drag_helpers;
15
15
  mod props;
16
16
  mod sheets;
17
17
 
18
- use std::cell::RefCell;
19
- use std::collections::HashSet;
18
+ use std::cell::{Cell, RefCell};
19
+ use std::collections::{BTreeMap, HashSet};
20
20
  use std::ops::Deref;
21
21
  use std::rc::Rc;
22
22
 
@@ -27,10 +27,13 @@ use web_sys::*;
27
27
  use yew::html::ImplicitClone;
28
28
  use yew::prelude::*;
29
29
 
30
- pub use self::column_locator::{ColumnLocator, ColumnSettingsTab, ColumnTab, OpenColumnSettings};
30
+ pub use self::column_locator::{
31
+ ColumnLocator, ColumnSettingsTab, ColumnSettingsTarget, ColumnTab, OpenColumnSettings,
32
+ };
31
33
  use self::drag_helpers::DragTargetState;
32
34
  pub use self::drag_helpers::{DragDropContainer, DragEndCallback};
33
35
  pub use self::props::{DragDropProps, PresentationProps};
36
+ use crate::config::{CssKind, NamedValue, assign_palette_names};
34
37
  use crate::utils::*;
35
38
 
36
39
  #[derive(Clone, Debug)]
@@ -63,6 +66,12 @@ impl DragState {
63
66
  pub struct PresentationHandle {
64
67
  viewer_elem: HtmlElement,
65
68
 
69
+ /// The embedded LLM agent's shared model (runtime + chat transcript) —
70
+ /// carried here so the settings sidebar can render the chat tab without
71
+ /// threading a new prop chain from the element.
72
+ #[cfg(feature = "llm-agent")]
73
+ pub agent: crate::agent::AgentSlot,
74
+
66
75
  /// The available themes as detected in the browser environment or set
67
76
  /// explicitly when CORS prevents detection — a MEMO of a document
68
77
  /// external, not component state. `None` until first parsed (detection
@@ -79,6 +88,18 @@ pub struct PresentationHandle {
79
88
  /// concurrent `get_available_themes` calls await one parse instead of
80
89
  /// racing their own.
81
90
  theme_init: Mutex<()>,
91
+
92
+ /// Whether the host's theme was ever EXPLICITLY chosen — authored as a
93
+ /// `theme` attribute, or set by name through [`Self::set_theme_name`].
94
+ ///
95
+ /// `false` means the host merely displays the registry default, which
96
+ /// [`Self::reset_themes`] is free to move; `true` pins the selection
97
+ /// until it leaves the registry. The `theme` attribute alone cannot
98
+ /// carry this, because `init` stamps it unconditionally so the document
99
+ /// cascade has a theme to match.
100
+ theme_selected: Cell<bool>,
101
+
102
+ palette: RefCell<BTreeMap<String, String>>,
82
103
  is_settings_open: RefCell<bool>,
83
104
  open_column_settings: RefCell<OpenColumnSettings>,
84
105
  is_workspace: RefCell<Option<bool>>,
@@ -103,11 +124,20 @@ pub struct PresentationHandle {
103
124
  /// dragged element from the shadow tree.
104
125
  host_dragend: RefCell<Option<DragEndCallback>>,
105
126
 
127
+ source_dragend: RefCell<Option<(web_sys::EventTarget, DragEndCallback)>>,
128
+
106
129
  /// IntersectionObserver-based fallback for the drag image, kept alive for
107
130
  /// the duration of the drag.
108
131
  drag_target: RefCell<Option<DragTargetState>>,
109
132
 
110
- pub settings_open_changed: PubSub<bool>,
133
+ /// `(open, announce)` — `announce` says this toggle is the SOLE carrier
134
+ /// of the config change (a user gesture: toolbar, `toggleConfig`), so
135
+ /// the element must emit `toggle-settings` + a config-update for it. An
136
+ /// API restore's toggle is announced by its own view-config commit
137
+ /// dispatch instead — announcing here too would double-emit, the first
138
+ /// carrying the intermediate config (settings flipped, view fields not
139
+ /// yet committed).
140
+ pub settings_open_changed: PubSub<(bool, bool)>,
111
141
 
112
142
  /// Injected callback from the root component, replacing the former
113
143
  /// `is_workspace_changed: PubSub` field.
@@ -148,8 +178,12 @@ impl Presentation {
148
178
  pub fn new(elem: &HtmlElement) -> Self {
149
179
  let theme = Self(Rc::new(PresentationHandle {
150
180
  viewer_elem: elem.clone(),
181
+ #[cfg(feature = "llm-agent")]
182
+ agent: Default::default(),
151
183
  themes: Default::default(),
152
184
  theme_init: Default::default(),
185
+ theme_selected: Cell::new(elem.get_attribute("theme").is_some()),
186
+ palette: Default::default(),
153
187
  is_workspace: Default::default(),
154
188
  settings_open_changed: Default::default(),
155
189
  settings_before_open_changed: Default::default(),
@@ -165,6 +199,7 @@ impl Presentation {
165
199
  on_dragstart: Default::default(),
166
200
  on_dragend: Default::default(),
167
201
  host_dragend: Default::default(),
202
+ source_dragend: Default::default(),
168
203
  drag_target: Default::default(),
169
204
  }));
170
205
 
@@ -230,13 +265,84 @@ impl Presentation {
230
265
  }
231
266
  }
232
267
 
233
- pub fn set_settings_open(&self, open: bool) {
234
- self.settings_open_changed.emit(open);
268
+ /// See [`PresentationHandle::settings_open_changed`] for `announce`.
269
+ pub fn set_settings_open(&self, open: bool, announce: bool) {
270
+ self.settings_open_changed.emit((open, announce));
235
271
  }
236
272
 
237
273
  /// Sets the currently opened column settings. Emits an internal event on
238
274
  /// change. Passing None is a shorthand for setting all fields to
239
275
  /// None.
276
+ /// The workspace palette as last restored (canonical values).
277
+ pub fn palette(&self) -> BTreeMap<String, String> {
278
+ self.0.palette.borrow().clone()
279
+ }
280
+
281
+ /// Replace the host palette: every previously-applied `--psp-user--*`
282
+ /// inline property is removed, then `palette` is applied.
283
+ pub fn set_palette(&self, palette: BTreeMap<String, String>) -> ApiResult<()> {
284
+ let style = self.0.viewer_elem.style();
285
+ for name in self.0.palette.borrow().keys() {
286
+ style.remove_property(name)?;
287
+ }
288
+
289
+ for (name, value) in &palette {
290
+ style.set_property(name, value)?;
291
+ }
292
+
293
+ *self.0.palette.borrow_mut() = palette;
294
+ Ok(())
295
+ }
296
+
297
+ /// Pin `literal` into the restored palette, named by the same rules
298
+ /// as the derived set and applied to the host.
299
+ pub fn pin_style(&self, kind: CssKind, literal: &str) -> ApiResult<()> {
300
+ let Ok(value) = kind.canonicalize(literal) else {
301
+ return Ok(());
302
+ };
303
+
304
+ let current = self.palette();
305
+ let host = self.host_named_values(kind);
306
+ let set = assign_palette_names(&current, &host, &[(kind, value)], &|name| {
307
+ self.resolve_css_var(name).is_some()
308
+ });
309
+
310
+ let style = self.0.viewer_elem.style();
311
+ for (name, value) in &set {
312
+ if !current.contains_key(name) {
313
+ style.set_property(name, value)?;
314
+ }
315
+ }
316
+
317
+ *self.0.palette.borrow_mut() = set;
318
+ Ok(())
319
+ }
320
+
321
+ /// The host's computed value for custom property `name` (inline
322
+ /// palette, then theme/page stylesheets), or `None` if undefined.
323
+ pub fn resolve_css_var(&self, name: &str) -> Option<String> {
324
+ read_custom_property(&self.0.viewer_elem, name)
325
+ }
326
+
327
+ /// Theme/page-authored named values of `kind` on the host, canonical,
328
+ /// discovered by the contiguous-numbering walk `--psp-user--<kind>-1`,
329
+ /// `-2`, … up to the first undefined name.
330
+ pub fn host_named_values(&self, kind: CssKind) -> Vec<NamedValue> {
331
+ let mut out = vec![];
332
+ for n in 1.. {
333
+ let name = format!("{}{n}", kind.var_prefix());
334
+ let Some(raw) = self.resolve_css_var(&name) else {
335
+ break;
336
+ };
337
+
338
+ if let Ok(value) = kind.canonicalize(&raw) {
339
+ out.push(NamedValue { name, value });
340
+ }
341
+ }
342
+
343
+ out
344
+ }
345
+
240
346
  pub fn set_open_column_settings(&self, settings: Option<OpenColumnSettings>) {
241
347
  let settings = settings.unwrap_or_default();
242
348
  if *(self.open_column_settings.borrow()) != settings {
@@ -307,22 +413,24 @@ impl Presentation {
307
413
  index.and_then(|x| themes.get(x).cloned())
308
414
  }
309
415
 
310
- /// The default theme (first registered), used to resolve a panel's
311
- /// effective theme when it has no per-panel override. `None` if no
416
+ /// The theme a NEW panel is born with: the host's if it has one, else
417
+ /// the registry default. Synchronous, because panel creation is — the
418
+ /// registry fallback is `None` until the registry first parses, which
419
+ /// [`crate::tasks::seed_panel_theme`] fills in.
420
+ pub fn active_theme_name_sync(&self) -> Option<String> {
421
+ self.0
422
+ .viewer_elem
423
+ .get_attribute("theme")
424
+ .or_else(|| self.0.themes.borrow().as_ref()?.first().cloned())
425
+ }
426
+
427
+ /// The registry default — the FIRST registered theme, which a panel or
428
+ /// host resolves to only when it has no theme of its own. `None` if no
312
429
  /// themes exist.
313
430
  pub async fn get_default_theme_name(&self) -> Option<String> {
314
431
  self.get_available_themes().await.ok()?.first().cloned()
315
432
  }
316
433
 
317
- /// SYNC read of the registry default theme name, derived on demand from
318
- /// the memoized registry — `None` until the registry first parses (or
319
- /// when no themes exist). For synchronous stamping paths that must not
320
- /// await registry init; prefer [`Self::get_default_theme_name`] where
321
- /// awaiting is acceptable.
322
- pub fn default_theme_name_sync(&self) -> Option<String> {
323
- self.0.themes.borrow().as_ref()?.first().cloned()
324
- }
325
-
326
434
  fn set_theme_attribute(&self, theme: Option<&str>) -> ApiResult<()> {
327
435
  if let Some(theme) = theme {
328
436
  Ok(self.0.viewer_elem.set_attribute("theme", theme)?)
@@ -333,12 +441,39 @@ impl Presentation {
333
441
 
334
442
  pub async fn reset_theme(&self) -> ApiResult<()> {
335
443
  *self.0.is_workspace.borrow_mut() = None;
336
- let themes = self.get_available_themes().await?;
337
- let default_theme = themes.first().map(|x| x.as_str());
338
- self.set_theme_name(default_theme).await?;
444
+ self.set_theme_name(None).await?;
339
445
  Ok(())
340
446
  }
341
447
 
448
+ /// Adopt `themes` as the available set, KEEPING the host's theme unless
449
+ /// it was never explicitly chosen or has left the set — the only two
450
+ /// cases in which re-ordering the registry may move the viewer.
451
+ ///
452
+ /// Always re-stamps; the caller publishes ([`Self::publish_theme_config`])
453
+ /// once its restyles have resolved, because the available list has
454
+ /// changed even when the selection has not.
455
+ ///
456
+ /// @param themes the new set, or `None` to re-parse the document.
457
+ ///
458
+ /// # Returns
459
+ /// The active theme after the change.
460
+ pub async fn reset_themes(&self, themes: Option<Vec<String>>) -> ApiResult<Option<String>> {
461
+ let selected = self
462
+ .0
463
+ .theme_selected
464
+ .get()
465
+ .then(|| self.0.viewer_elem.get_attribute("theme"))
466
+ .flatten();
467
+
468
+ self.reset_available_themes(themes).await;
469
+ let available = self.get_available_themes().await?;
470
+ let kept = selected.filter(|name| available.contains(name));
471
+ self.0.theme_selected.set(kept.is_some());
472
+ let active = kept.or_else(|| available.first().cloned());
473
+ self.set_theme_attribute(active.as_deref())?;
474
+ Ok(active)
475
+ }
476
+
342
477
  /// Set the theme by name, or `None` for the default theme.
343
478
  ///
344
479
  /// A NAMED theme's host attribute write is SYNCHRONOUS ("stamp with
@@ -360,6 +495,7 @@ impl Presentation {
360
495
  /// # Returns
361
496
  /// A `bool` indicating whether the internal state changed.
362
497
  pub async fn set_theme_name(&self, theme: Option<&str>) -> ApiResult<bool> {
498
+ self.0.theme_selected.set(theme.is_some());
363
499
  if let Some(theme) = theme {
364
500
  if self.0.viewer_elem.get_attribute("theme").as_deref() == Some(theme) {
365
501
  return Ok(false);
@@ -369,20 +505,27 @@ impl Presentation {
369
505
  }
370
506
 
371
507
  let themes = self.get_available_themes().await?;
372
- let index = if let Some(theme) = theme {
373
- themes.iter().position(|x| x == theme)
374
- } else if !themes.is_empty() {
508
+ if theme.is_none() {
375
509
  self.set_theme_attribute(themes.first().map(|x| x.as_str()))?;
376
- Some(0)
377
- } else {
378
- self.set_theme_attribute(None)?;
379
- None
380
- };
510
+ }
381
511
 
382
- self.theme_config_updated.emit((themes, index));
383
512
  Ok(true)
384
513
  }
385
514
 
515
+ /// Publish the `theme_config_updated` snapshot — the available themes
516
+ /// and the host's current selection — to the component tree.
517
+ pub async fn publish_theme_config(&self) -> ApiResult<()> {
518
+ let themes = self.get_available_themes().await?;
519
+ let index = self
520
+ .0
521
+ .viewer_elem
522
+ .get_attribute("theme")
523
+ .and_then(|active| themes.iter().position(|x| *x == active));
524
+
525
+ self.theme_config_updated.emit((themes, index));
526
+ Ok(())
527
+ }
528
+
386
529
  /// Snapshot the drag state as a [`DragDropProps`] value for threading
387
530
  /// through the component tree without PubSub subscriptions.
388
531
  pub fn drag_drop_props(&self) -> DragDropProps {
@@ -419,6 +562,7 @@ impl Presentation {
419
562
 
420
563
  pub fn set_drag_image(&self, event: &DragEvent) -> ApiResult<()> {
421
564
  event.stop_propagation();
565
+ self.register_source_dragend(event)?;
422
566
  if let Some(dt) = event.data_transfer() {
423
567
  dt.set_drop_effect("move");
424
568
  }
@@ -477,8 +621,7 @@ impl Presentation {
477
621
  _ => None,
478
622
  };
479
623
 
480
- self.drag_target.borrow_mut().take();
481
- *self.drag_state.borrow_mut() = DragState::NoDrag;
624
+ self.end_drag();
482
625
  if let Some(action) = action {
483
626
  self.drop_received.emit(action);
484
627
  }
@@ -502,11 +645,15 @@ impl Presentation {
502
645
  /// End the drag/drop action by resetting the state to default.
503
646
  pub fn notify_drag_end(&self) {
504
647
  if self.drag_state.borrow().is_drag_in_progress() {
505
- self.drag_target.borrow_mut().take();
506
- *self.drag_state.borrow_mut() = DragState::NoDrag;
507
- if let Some(cb) = self.on_dragend.borrow().as_ref() {
508
- cb.emit(());
509
- }
648
+ self.end_drag();
649
+ }
650
+ }
651
+
652
+ fn end_drag(&self) {
653
+ self.drag_target.borrow_mut().take();
654
+ *self.drag_state.borrow_mut() = DragState::NoDrag;
655
+ if let Some(cb) = self.on_dragend.borrow().as_ref() {
656
+ cb.emit(());
510
657
  }
511
658
  }
512
659
 
@@ -514,6 +661,23 @@ impl Presentation {
514
661
  /// element so that drag-end cleanup fires even when Yew re-renders
515
662
  /// remove the original dragged element from the shadow DOM. The host
516
663
  /// element is outside the virtual DOM and therefore stable.
664
+ fn register_source_dragend(&self, event: &DragEvent) -> ApiResult<()> {
665
+ let target = event.target().into_apierror()?;
666
+ if let Some((prev_target, prev)) = self.source_dragend.borrow_mut().take() {
667
+ let _ = prev_target
668
+ .remove_event_listener_with_callback("dragend", prev.as_ref().unchecked_ref());
669
+ }
670
+
671
+ let this = self.clone();
672
+ let closure = Closure::wrap(Box::new(move |_event: DragEvent| {
673
+ this.notify_drag_end();
674
+ }) as Box<dyn FnMut(DragEvent)>);
675
+
676
+ target.add_event_listener_with_callback("dragend", closure.as_ref().unchecked_ref())?;
677
+ *self.source_dragend.borrow_mut() = Some((target, closure));
678
+ Ok(())
679
+ }
680
+
517
681
  fn register_host_dragend(&self) {
518
682
  if let Some(prev) = self.host_dragend.borrow_mut().take() {
519
683
  let _ = self
@@ -12,26 +12,29 @@
12
12
 
13
13
  use perspective_client::config::ViewConfig;
14
14
 
15
- use crate::presentation::{ColumnLocator, OpenColumnSettings};
15
+ use crate::presentation::{ColumnLocator, ColumnSettingsTarget, OpenColumnSettings};
16
16
  use crate::renderer::Renderer;
17
17
  use crate::session::SessionMetadata;
18
18
 
19
- /// Returns a [`ColumnLocator`] for a given column name, or [`None`] if no
20
- /// such column exists.
21
- pub fn get_column_locator(
19
+ /// Classify a column name against the session, config-first: the view
20
+ /// config is the commit of record for expressions and windows.
21
+ pub fn classify_column(
22
+ name: &str,
23
+ view_config: &ViewConfig,
22
24
  metadata: &SessionMetadata,
23
- name: Option<String>,
24
25
  ) -> Option<ColumnLocator> {
25
- name.and_then(|name| {
26
- if metadata.is_column_expression(&name) {
27
- Some(ColumnLocator::Expression(name))
28
- } else {
29
- metadata.get_table_columns().and_then(|x| {
30
- x.iter()
31
- .find_map(|n| (n == &name).then_some(ColumnLocator::Table(name.clone())))
32
- })
33
- }
34
- })
26
+ if view_config.windows.contains_key(name) || metadata.is_column_window(name) {
27
+ Some(ColumnLocator::Window(name.to_owned()))
28
+ } else if view_config.expressions.contains_key(name) || metadata.is_column_expression(name) {
29
+ Some(ColumnLocator::Expression(name.to_owned()))
30
+ } else if metadata
31
+ .get_table_columns()
32
+ .is_some_and(|cols| cols.iter().any(|col| col == name))
33
+ {
34
+ Some(ColumnLocator::Table(name.to_owned()))
35
+ } else {
36
+ None
37
+ }
35
38
  }
36
39
 
37
40
  /// Gets a [`ColumnLocator`] for the current UI's column settings state,
@@ -43,35 +46,62 @@ pub fn get_current_column_locator(
43
46
  open_column_settings: &OpenColumnSettings,
44
47
  renderer: &Renderer,
45
48
  view_config: &ViewConfig,
46
- _metadata: &SessionMetadata,
49
+ metadata: &SessionMetadata,
47
50
  ) -> Option<ColumnLocator> {
48
- open_column_settings
49
- .locator
50
- .clone()
51
- .filter(|locator| match locator {
52
- ColumnLocator::Table(_name) => {
53
- locator
54
- .name()
55
- .map(|name| {
56
- view_config.columns.iter().any(|maybe_col| {
57
- maybe_col
58
- .as_ref()
59
- .map(|col| col == name)
60
- .unwrap_or_default()
51
+ match open_column_settings.target.as_ref()? {
52
+ ColumnSettingsTarget::NewExpression => Some(ColumnLocator::NewExpression),
53
+ ColumnSettingsTarget::Column(name) => {
54
+ let locator = classify_column(name, view_config, metadata)?;
55
+ match locator {
56
+ ColumnLocator::Table(_) => {
57
+ let in_columns = view_config.columns.iter().any(|maybe_col| {
58
+ maybe_col
59
+ .as_ref()
60
+ .map(|col| col == name)
61
+ .unwrap_or_default()
62
+ });
63
+
64
+ (in_columns && renderer.can_render_column_styles()).then_some(locator)
65
+ },
66
+ locator => Some(locator),
67
+ }
68
+ },
69
+ }
70
+ }
71
+
72
+ #[cfg(test)]
73
+ mod tests {
74
+ use std::collections::HashMap;
75
+
76
+ use perspective_client::config::Expressions;
77
+
78
+ use super::*;
79
+
80
+ fn config_with_expression(name: &str, expr: &str) -> ViewConfig {
81
+ ViewConfig {
82
+ expressions: Expressions(HashMap::from([(name.to_owned(), expr.to_owned())])),
83
+ ..ViewConfig::default()
84
+ }
85
+ }
86
+
87
+ #[test]
88
+ fn classifies_from_the_config_before_metadata_catches_up() {
89
+ let config = config_with_expression("x", "1 + 1");
90
+ let metadata = SessionMetadata::default();
91
+ assert_eq!(
92
+ classify_column("x", &config, &metadata),
93
+ Some(ColumnLocator::Expression("x".to_owned()))
94
+ );
95
+ assert_eq!(classify_column("nope", &config, &metadata), None);
96
+ }
61
97
 
62
- // }) || view_config.group_by.iter().any(|col|
63
- // col == name) ||
64
- // view_config.split_by.iter().any(|col| col ==
65
- // name) ||
66
- // view_config.filter.iter().any(|col| col.column()
67
- // == name) ||
68
- // view_config.sort.iter().any(|col| &col.0 == name)
69
- // })
70
- })
71
- })
72
- .unwrap_or_default()
73
- && renderer.can_render_column_styles()
74
- },
75
- _ => true,
76
- })
98
+ #[test]
99
+ fn unnamed_expressions_are_keyed_by_their_text() {
100
+ let config = config_with_expression("\"Sales\" + 1", "\"Sales\" + 1");
101
+ let metadata = SessionMetadata::default();
102
+ assert_eq!(
103
+ classify_column("\"Sales\" + 1", &config, &metadata),
104
+ Some(ColumnLocator::Expression("\"Sales\" + 1".to_owned()))
105
+ );
106
+ }
77
107
  }