@perspective-dev/viewer 5.0.0 → 5.2.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 (213) 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 +2 -0
  26. package/dist/esm/perspective-viewer.inline.js +8 -8
  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/providers.d.ts +79 -0
  31. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  32. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  33. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  34. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  35. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  36. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  37. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  38. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  39. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  40. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  41. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  42. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  43. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  44. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  45. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +43 -1
  46. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  47. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  48. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  49. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  50. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  51. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  52. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  53. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  54. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  55. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  56. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  57. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  58. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  59. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  60. package/dist/esm/ts-rs/ViewerConfig.d.ts +43 -1
  61. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  62. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +72 -0
  63. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  64. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  65. package/dist/esm/ts-rs/WindowSpec.d.ts +47 -0
  66. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  67. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
  68. package/dist/wasm/perspective-viewer.d.ts +222 -26
  69. package/dist/wasm/perspective-viewer.js +378 -125
  70. package/dist/wasm/perspective-viewer.wasm +0 -0
  71. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
  72. package/package.json +2 -1
  73. package/src/css/_viewer_bundle.css +1 -0
  74. package/src/css/column-selector.css +9 -0
  75. package/src/css/column-settings-panel.css +67 -2
  76. package/src/css/column-style.css +4 -3
  77. package/src/css/config-selector.css +60 -4
  78. package/src/css/containers/context-menu.css +12 -6
  79. package/src/css/containers/tabs.css +2 -2
  80. package/src/css/form/code-editor.css +2 -3
  81. package/src/css/form/mirrored-textarea.css +81 -0
  82. package/src/css/viewer.css +336 -0
  83. package/src/rust/agent/client/mod.rs +19 -0
  84. package/src/rust/agent/client/protocol.rs +320 -0
  85. package/src/rust/agent/client/transport.rs +287 -0
  86. package/src/rust/agent/client/turn.rs +174 -0
  87. package/src/rust/agent/config.rs +159 -0
  88. package/src/rust/agent/docs.rs +293 -0
  89. package/src/rust/agent/markdown/block.rs +509 -0
  90. package/src/rust/agent/markdown/entity.rs +52 -0
  91. package/src/rust/agent/markdown/events.rs +71 -0
  92. package/src/rust/agent/markdown/inline.rs +497 -0
  93. package/src/rust/agent/markdown/mod.rs +466 -0
  94. package/src/rust/agent/mod.rs +23 -0
  95. package/src/rust/agent/preamble.md +16 -0
  96. package/src/rust/agent/runtime.rs +201 -0
  97. package/src/rust/agent/slot.rs +236 -0
  98. package/src/rust/agent/tools.rs +1110 -0
  99. package/src/rust/components/chat_panel.rs +289 -0
  100. package/src/rust/components/column_selector/active_column.rs +21 -27
  101. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  102. package/src/rust/components/column_selector/config_selector.rs +83 -8
  103. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  104. package/src/rust/components/column_selector/inactive_column.rs +8 -1
  105. package/src/rust/components/column_selector.rs +27 -3
  106. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  107. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  108. package/src/rust/components/column_settings_sidebar.rs +163 -16
  109. package/src/rust/components/containers/dragdrop_list.rs +110 -3
  110. package/src/rust/components/containers/sidebar.rs +99 -22
  111. package/src/rust/components/containers/split_panel.rs +6 -0
  112. package/src/rust/components/editable_header.rs +2 -0
  113. package/src/rust/components/form/code_editor.rs +14 -23
  114. package/src/rust/components/form/debug.rs +8 -5
  115. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  116. package/src/rust/components/form/mod.rs +1 -0
  117. package/src/rust/components/main_panel.rs +5 -0
  118. package/src/rust/components/mod.rs +3 -0
  119. package/src/rust/components/panel_menu.rs +7 -0
  120. package/src/rust/components/panel_tab.rs +5 -0
  121. package/src/rust/components/portal.rs +7 -0
  122. package/src/rust/components/settings_panel.rs +80 -2
  123. package/src/rust/components/viewer/msg.rs +2 -0
  124. package/src/rust/components/viewer/panels.rs +9 -14
  125. package/src/rust/components/viewer/render.rs +14 -0
  126. package/src/rust/components/viewer/settings.rs +33 -1
  127. package/src/rust/components/viewer.rs +2 -0
  128. package/src/rust/components/window_editor.rs +985 -0
  129. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  130. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  131. package/src/rust/config/number_string_format.rs +33 -0
  132. package/src/rust/config/options.rs +18 -0
  133. package/src/rust/config/plugin_static_config.rs +120 -5
  134. package/src/rust/config/viewer_config.rs +220 -1
  135. package/src/rust/config/workspace_config.rs +7 -6
  136. package/src/rust/custom_elements/viewer.rs +209 -18
  137. package/src/rust/custom_events.rs +29 -0
  138. package/src/rust/lib.rs +17 -1
  139. package/src/rust/presentation/column_locator.rs +16 -1
  140. package/src/rust/presentation.rs +8 -0
  141. package/src/rust/queries/columns_iter_set.rs +7 -1
  142. package/src/rust/queries/get_viewer_config.rs +5 -1
  143. package/src/rust/renderer/dispatch.rs +85 -58
  144. package/src/rust/renderer.rs +34 -1
  145. package/src/rust/session/column_defaults_update.rs +18 -0
  146. package/src/rust/session/drag_drop_update.rs +18 -0
  147. package/src/rust/session/metadata.rs +64 -9
  148. package/src/rust/session/replace_expression_update.rs +2 -0
  149. package/src/rust/session.rs +7 -5
  150. package/src/rust/tasks/create_panel.rs +18 -8
  151. package/src/rust/tasks/edit_window.rs +155 -0
  152. package/src/rust/tasks/mod.rs +2 -0
  153. package/src/rust/tasks/restore_panel.rs +34 -17
  154. package/src/rust/tasks/sync_update_panels.rs +7 -23
  155. package/src/rust/utils/browser/dragdrop.rs +12 -0
  156. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  157. package/src/rust/utils/debounce.rs +76 -27
  158. package/src/rust/workspace.rs +112 -20
  159. package/src/svg/pin-icon.svg +5 -0
  160. package/src/themes/icons.css +1 -0
  161. package/src/themes/intl/de.css +23 -0
  162. package/src/themes/intl/es.css +23 -0
  163. package/src/themes/intl/fr.css +23 -0
  164. package/src/themes/intl/ja.css +23 -0
  165. package/src/themes/intl/pt.css +23 -0
  166. package/src/themes/intl/zh.css +23 -0
  167. package/src/themes/intl.css +26 -1
  168. package/src/ts/column-format.ts +67 -46
  169. package/src/ts/perspective-viewer.ts +2 -0
  170. package/src/ts/providers.ts +96 -0
  171. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  172. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  173. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  174. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  175. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  176. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  177. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  178. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  179. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  180. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  181. package/src/ts/ts-rs/FormatMode.ts +3 -0
  182. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  183. package/src/ts/ts-rs/Notation.ts +11 -0
  184. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  185. package/src/ts/ts-rs/PanelViewerConfig.ts +48 -1
  186. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  187. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  188. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  189. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  190. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  191. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  192. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  193. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  194. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  195. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  196. package/src/ts/ts-rs/Unit.ts +3 -0
  197. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  198. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  199. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  200. package/src/ts/ts-rs/ViewerConfig.ts +48 -1
  201. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  202. package/src/ts/ts-rs/ViewerConfigUpdate.ts +81 -2
  203. package/src/ts/ts-rs/WindowSort.ts +9 -0
  204. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  205. package/src/ts/ts-rs/WindowSpec.ts +47 -0
  206. package/src/ts/ts-rs/Windows.ts +10 -0
  207. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
  208. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
  209. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
  210. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
  211. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
  212. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
  213. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
@@ -52,6 +52,31 @@ where
52
52
 
53
53
  #[prop_or_default]
54
54
  pub allow_duplicates: bool,
55
+
56
+ /// Single-slot mode: the list holds at most one item, a dragover
57
+ /// preview REPLACES the current item rather than inserting beside it,
58
+ /// and the trailing `EmptyColumn` autocomplete renders only while the
59
+ /// slot is empty.
60
+ #[prop_or_default]
61
+ pub single_slot: bool,
62
+
63
+ /// The plugin-declared visual role this slot fills, e.g. `"X Axis"`
64
+ /// for a `Y Line`'s `group_by` (see `PluginStaticConfig`). Rendered
65
+ /// as the slot's label THROUGH the intl indirection: the role is a
66
+ /// key, not display text, so `--psp-label--role--x-axis--content`
67
+ /// supplies the words and a language variant can override them. A
68
+ /// role with no such label falls back to the declared English, and
69
+ /// no role at all falls back to the slot's own generic label.
70
+ #[prop_or_default]
71
+ pub role_label: Option<AttrValue>,
72
+
73
+ /// The in-flight drag is INVALID for this list (a parent-defined rule,
74
+ /// e.g. the window editor's Table-columns-only slots): the dragover
75
+ /// preview is suppressed and the invalid-X overlay renders instead,
76
+ /// like a duplicate drag over `group_by`/`split_by`. The parent's drop
77
+ /// handler is still responsible for ignoring the drop itself.
78
+ #[prop_or_default]
79
+ pub is_invalid: bool,
55
80
  }
56
81
 
57
82
  impl<T, U> PartialEq for DragDropListProps<T, U>
@@ -66,6 +91,9 @@ where
66
91
  && self.allow_duplicates == other.allow_duplicates
67
92
  && self.is_dragover == other.is_dragover
68
93
  && self.disabled == other.disabled
94
+ && self.single_slot == other.single_slot
95
+ && self.is_invalid == other.is_invalid
96
+ && self.role_label == other.role_label
69
97
  }
70
98
  }
71
99
 
@@ -202,7 +230,51 @@ where
202
230
  let invalid_drag: bool;
203
231
  let mut valid_duplicate_drag = false;
204
232
 
205
- let columns_html = {
233
+ let columns_html = if ctx.props().single_slot {
234
+ invalid_drag = ctx.props().is_invalid && ctx.props().is_dragover.is_some();
235
+
236
+ // Dragging the slot's own pill over its own slot is a no-op
237
+ // move - keep showing the pill instead of the drop preview
238
+ // (mirrors the multi-column branch's `is_self_move` handling).
239
+ let is_self_move = ctx
240
+ .props()
241
+ .presentation
242
+ .get_drag_target()
243
+ .map(|x| V::is_self_move(x))
244
+ .unwrap_or_default();
245
+
246
+ let close = ctx.props().parent.callback(|_| V::close(0));
247
+ let dragenter = ctx.props().parent.callback({
248
+ let container_noderef = container_noderef.clone();
249
+ move |event: DragEvent| {
250
+ event.stop_propagation();
251
+ event.prevent_default();
252
+ if event.related_target().is_none()
253
+ && let Some(elem) = container_noderef.cast::<HtmlElement>()
254
+ {
255
+ let _ = elem.dataset().set("safaridragleave", "true");
256
+ }
257
+ V::dragenter(0)
258
+ }
259
+ });
260
+
261
+ if ctx.props().is_dragover.is_some() && !is_self_move && !invalid_drag {
262
+ html! {
263
+ <div class="pivot-column" ondragenter={dragenter}>
264
+ <div class="config-drop" />
265
+ </div>
266
+ }
267
+ } else if let Some(column) = ctx.props().children.iter().next() {
268
+ html! {
269
+ <div class="pivot-column" ondragenter={dragenter}>
270
+ { Html::from(column) }
271
+ <span class="row_close" onmousedown={close} />
272
+ </div>
273
+ }
274
+ } else {
275
+ html! {}
276
+ }
277
+ } else {
206
278
  let mut columns = ctx
207
279
  .props()
208
280
  .children
@@ -211,7 +283,10 @@ where
211
283
  .enumerate()
212
284
  .collect::<Vec<_>>();
213
285
 
214
- invalid_drag = if let Some((x, column)) = &ctx.props().is_dragover {
286
+ invalid_drag = if ctx.props().is_invalid && ctx.props().is_dragover.is_some() {
287
+ // Parent-defined invalidity: no preview, X overlay only.
288
+ true
289
+ } else if let Some((x, column)) = &ctx.props().is_dragover {
215
290
  let index = *x;
216
291
  let is_append = index == columns.len();
217
292
  let is_self_move = ctx
@@ -314,16 +389,48 @@ where
314
389
  .collect::<Html>()
315
390
  };
316
391
 
392
+ let show_empty = if ctx.props().single_slot {
393
+ ctx.props().children.is_empty() && ctx.props().is_dragover.is_none()
394
+ } else {
395
+ ctx.props().is_dragover.is_none() | (!invalid_drag && valid_duplicate_drag)
396
+ };
397
+
317
398
  let column_dropdown = ctx.props().column_dropdown.clone();
318
399
  let exclude = ctx.props().exclude.clone();
319
400
  let on_select = ctx.props().parent.callback(V::create);
320
401
  let class = classes!("rrow");
321
402
  let is_enabled = true;
322
403
 
404
+ // The role is data from the plugin, so the var NAME is built
405
+ // here while the var VALUE stays in the stylesheets - custom
406
+ // properties inherit, so the label's `:before` picks it up.
407
+ let role_style = ctx.props().role_label.as_ref().map(|role| {
408
+ let slug = role
409
+ .chars()
410
+ .map(|x| {
411
+ if x.is_ascii_alphanumeric() {
412
+ x.to_ascii_lowercase()
413
+ } else {
414
+ '-'
415
+ }
416
+ })
417
+ .collect::<String>();
418
+
419
+ // The second var is a PRESENCE FLAG: CSS cannot ask whether a
420
+ // custom property is set, so the slot's stylesheet reads it as
421
+ // the secondary label's `display` and gets `none` by fallback
422
+ // when no role was declared.
423
+ format!(
424
+ "--psp-label--pivot--content: var(--psp-label--role--{slug}--content, \
425
+ \"{role}\"); --psp-label--pivot-secondary--display: inline-block"
426
+ )
427
+ });
428
+
323
429
  html! {
324
430
  <div ref={&self.elem} {class}>
325
431
  <div
326
432
  id={ctx.props().name}
433
+ style={role_style}
327
434
  ondragover={is_enabled.then_some(dragover)}
328
435
  ondragenter={is_enabled.then_some(drag_container.dragenter)}
329
436
  ondragleave={is_enabled.then_some(drag_container.dragleave)}
@@ -345,7 +452,7 @@ where
345
452
  onmousedown={ctx.props().parent.callback(move |_| V::close(0))}
346
453
  />
347
454
  </div>
348
- } else if ctx.props().is_dragover.is_none() | (!invalid_drag && valid_duplicate_drag) {
455
+ } else if show_empty {
349
456
  <EmptyColumn {column_dropdown} {exclude} {on_select} />
350
457
  } else if invalid_drag {
351
458
  <InvalidColumn />
@@ -11,14 +11,17 @@
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
13
  use perspective_client::clone;
14
- use web_sys::Element;
14
+ use wasm_bindgen::JsCast;
15
+ use wasm_bindgen::prelude::Closure;
16
+ use web_sys::HtmlElement;
15
17
  use yew::{
16
- Callback, Children, Html, Properties, function_component, html, use_effect_with, use_node_ref,
17
- use_state_eq,
18
+ Callback, Children, Html, Properties, function_component, html, use_effect_with, use_mut_ref,
19
+ use_node_ref,
18
20
  };
19
21
 
20
22
  use crate::components::containers::sidebar_close_button::SidebarCloseButton;
21
23
  use crate::components::editable_header::{EditableHeader, EditableHeaderProps};
24
+ use crate::js::{ResizeObserver, ResizeObserverEntry};
22
25
 
23
26
  #[derive(PartialEq, Clone, Properties)]
24
27
  pub struct SidebarProps {
@@ -31,6 +34,28 @@ pub struct SidebarProps {
31
34
  pub width_override: Option<i32>,
32
35
  pub selected_tab: Option<usize>,
33
36
  pub header_props: EditableHeaderProps,
37
+
38
+ /// Trap-door width shared across this sidebar's tabs: the lifted
39
+ /// running max of the widths this component reports through
40
+ /// `on_auto_width`. Held by the parent (ultimately
41
+ /// `PerspectiveViewer`'s geometry state, like the settings panel's
42
+ /// Query/Plugin/Debug trap-door) so it survives tab switches AND
43
+ /// sidebar re-mounts, and clears on divider reset.
44
+ #[prop_or_default]
45
+ pub auto_width: f64,
46
+
47
+ /// Fires with the sidebar's rendered width after each render; the
48
+ /// owner keeps the running max threaded back as `auto_width`.
49
+ #[prop_or_default]
50
+ pub on_auto_width: Callback<f64>,
51
+
52
+ /// Pinned state for the header's pin toggle; the button renders only
53
+ /// when `on_toggle_pin` is provided.
54
+ #[prop_or_default]
55
+ pub is_pinned: bool,
56
+
57
+ #[prop_or_default]
58
+ pub on_toggle_pin: Option<Callback<()>>,
34
59
  }
35
60
 
36
61
  /// Sidebars are designed to live in a
@@ -39,33 +64,85 @@ pub struct SidebarProps {
39
64
  pub fn Sidebar(p: &SidebarProps) -> Html {
40
65
  let id = &p.id_prefix;
41
66
  let noderef = use_node_ref();
42
- let auto_width = use_state_eq(|| 0f64);
43
-
44
- // this gets the last calculated width and ensures that
45
- // the auto-width element is at least that big.
46
- // this ensures the panel grows but does not shrink.
47
- use_effect_with(p.clone(), {
48
- clone!(noderef, auto_width);
49
- move |p| {
50
- if p.width_override.is_none() {
51
- let updated_width = noderef
52
- .cast::<Element>()
53
- .map(|el| el.get_bounding_client_rect().width())
54
- .unwrap_or_default();
55
- let new_auto_width = (*auto_width).max(updated_width);
56
- auto_width.set(new_auto_width);
57
- } else {
58
- auto_width.set(0f64);
67
+
68
+ // The trap-door reports the sidebar's rendered width to its owner via
69
+ // a `ResizeObserver` on the sidebar element, NOT a render effect:
70
+ // width changes are driven by DOM mutations anywhere in the tab
71
+ // subtree (e.g. the window editor staging a long column name into a
72
+ // slot), which need not re-render this component at all. The observer
73
+ // sees every one. `contentRect` (not the border box) is load-bearing:
74
+ // the sizer below is a content child, so ratcheting the border box
75
+ // would feed any sidebar padding back into unbounded growth. A manual
76
+ // divider drag (`width_override`) disables the ratchet until the
77
+ // divider resets.
78
+ let live_props = use_mut_ref(|| (Callback::<f64>::default(), None::<i32>));
79
+ *live_props.borrow_mut() = (p.on_auto_width.clone(), p.width_override);
80
+ use_effect_with((), {
81
+ clone!(noderef, live_props);
82
+ move |_| {
83
+ let closure =
84
+ Closure::<dyn FnMut(js_sys::Array)>::new(move |entries: js_sys::Array| {
85
+ let (on_auto_width, width_override) = live_props.borrow().clone();
86
+ if width_override.is_none() {
87
+ for entry in entries.iter() {
88
+ let entry: ResizeObserverEntry = entry.unchecked_into();
89
+ on_auto_width.emit(entry.content_rect().width());
90
+ }
91
+ }
92
+ });
93
+
94
+ let observer = ResizeObserver::new(closure.as_ref().unchecked_ref());
95
+ let elem = noderef.cast::<HtmlElement>();
96
+ if let Some(elem) = &elem {
97
+ observer.observe(elem);
98
+ }
99
+
100
+ move || {
101
+ if let Some(elem) = &elem {
102
+ observer.unobserve(elem);
103
+ }
104
+
105
+ drop(closure);
59
106
  }
60
107
  }
61
108
  });
62
109
 
63
- let width_style = format!("min-width: 200px; width: {}px", *auto_width);
110
+ let auto_width = if p.width_override.is_none() {
111
+ p.auto_width
112
+ } else {
113
+ 0.0
114
+ };
115
+
116
+ let width_style = format!("min-width: 200px; width: {}px", auto_width);
117
+ let pin_button = p.on_toggle_pin.as_ref().map(|cb| {
118
+ let onclick = {
119
+ let cb = cb.clone();
120
+ Callback::from(move |_: web_sys::MouseEvent| cb.emit(()))
121
+ };
122
+
123
+ let mut class = yew::classes!("sidebar_pin_button");
124
+ if p.is_pinned {
125
+ class.push("is-pinned");
126
+ }
127
+
128
+ html! {
129
+ <span
130
+ id={format!("{id}_pin_button")}
131
+ {class}
132
+ title={if p.is_pinned { "Unpin" } else { "Pin" }}
133
+ {onclick}
134
+ />
135
+ }
136
+ });
137
+
64
138
  html! {
65
139
  <>
66
140
  <SidebarCloseButton id={format!("{id}_close_button")} on_close_sidebar={&p.on_close} />
67
141
  <div class="sidebar_column" id={format!("{id}_sidebar")} ref={noderef}>
68
- <div class="sidebar_header"><EditableHeader ..p.header_props.clone() /></div>
142
+ <div class="sidebar_header">
143
+ <EditableHeader ..p.header_props.clone() />
144
+ { pin_button }
145
+ </div>
69
146
  <div class="sidebar_border" id={format!("{id}_border")} />
70
147
  { p.children.iter().collect::<Html>() }
71
148
  <div class="sidebar-auto-width" style={width_style} />
@@ -26,6 +26,10 @@ pub struct SplitPanelProps {
26
26
  #[prop_or_default]
27
27
  pub id: Option<String>,
28
28
 
29
+ /// Extra classes on the root element (ignored under `no_wrap`).
30
+ #[prop_or_default]
31
+ pub class: Classes,
32
+
29
33
  #[prop_or_default]
30
34
  pub orientation: Orientation,
31
35
 
@@ -110,6 +114,7 @@ impl SplitPanelProps {
110
114
  impl PartialEq for SplitPanelProps {
111
115
  fn eq(&self, other: &Self) -> bool {
112
116
  self.id == other.id
117
+ && self.class == other.class
113
118
  && self.children == other.children
114
119
  && self.orientation == other.orientation
115
120
  && self.reverse == other.reverse
@@ -305,6 +310,7 @@ impl Component for SplitPanel {
305
310
  }
306
311
 
307
312
  let mut classes = classes!("split-panel");
313
+ classes.extend(ctx.props().class.clone());
308
314
  if orientation == Orientation::Vertical {
309
315
  classes.push("orient-vertical");
310
316
  }
@@ -119,10 +119,12 @@ impl Component for EditableHeader {
119
119
  }
120
120
  let metadata = &ctx.props().metadata;
121
121
  let expressions = metadata.get_expression_columns();
122
+ let windows = metadata.get_window_columns();
122
123
  let found = metadata
123
124
  .get_table_columns()?
124
125
  .iter()
125
126
  .chain(expressions)
127
+ .chain(windows)
126
128
  .contains(&new_value);
127
129
  Some(!found)
128
130
  })()
@@ -18,6 +18,7 @@ use web_sys::*;
18
18
  use yew::prelude::*;
19
19
 
20
20
  use crate::components::form::highlight::highlight;
21
+ use crate::components::form::mirrored_textarea::MirroredTextarea;
21
22
  use crate::components::function_dropdown::{FunctionDropDownElement, FunctionDropDownPortal};
22
23
  use crate::exprtk::{Cursor, tokenize};
23
24
  use crate::utils::*;
@@ -174,30 +175,20 @@ pub fn code_editor(props: &CodeEditorProps) -> Html {
174
175
  <>
175
176
  <div id="editor" {class}>
176
177
  <div id="line_numbers" ref={lineno_ref}>{ line_numbers }</div>
177
- <div id="editor-inner" {class}>
178
- <textarea
179
- {disabled}
180
- id="textarea_editable"
181
- class="scrollable"
182
- ref={textarea_ref}
183
- spellcheck="false"
184
- {oninput}
185
- {onscroll}
186
- {onkeydown}
187
- />
178
+ <MirroredTextarea
179
+ id="textarea_editable"
180
+ mirror_id="content"
181
+ class={classes!("editor-inner", class)}
182
+ mirror={terms}
183
+ {textarea_ref}
184
+ mirror_ref={content_ref}
185
+ {disabled}
186
+ {oninput}
187
+ {onscroll}
188
+ {onkeydown}
189
+ >
188
190
  <div id="editor-height-sizer" />
189
- <pre id="content" ref={content_ref}>
190
- { terms }
191
- { {
192
- // A linebreak which pushs a textarea into scroll overflow
193
- // may not necessarily do so in the `<pre>`, because there is
194
- // no cursor when the last line has no content, so add
195
- // some space here to make sure overlfow is in sync
196
- // with the text area.
197
- " "
198
- } }
199
- </pre>
200
- </div>
191
+ </MirroredTextarea>
201
192
  </div>
202
193
  <FunctionDropDownPortal
203
194
  element={(*portal_dropdown).clone()}
@@ -23,12 +23,14 @@ use crate::presentation::*;
23
23
  use crate::renderer::*;
24
24
  use crate::session::*;
25
25
  use crate::utils::*;
26
+ use crate::workspace::Workspace;
26
27
 
27
28
  #[derive(Clone, PartialEq, Properties)]
28
29
  pub struct DebugPanelProps {
29
30
  pub presentation: Presentation,
30
31
  pub renderer: Renderer,
31
32
  pub session: Session,
33
+ pub workspace: Workspace,
32
34
 
33
35
  /// Trap-door width pinned by the parent `SettingsPanel` so switching
34
36
  /// tabs doesn't shrink the panel. Threaded into the hidden sizer
@@ -236,15 +238,16 @@ impl DebugPanelProps {
236
238
  ApiFuture::spawn(async move {
237
239
  match serde_json::from_str(&text) {
238
240
  Ok(config) => {
239
- match crate::tasks::restore_and_render(
241
+ let active =
242
+ props.workspace.active_renderer().as_ref() == Some(&props.renderer);
243
+ match crate::tasks::restore_panel(
240
244
  &props.session,
241
245
  &props.renderer,
242
246
  &props.presentation,
243
- // A user-pasted config apply — an explicit request,
244
- // same affordance as the public `restore()`.
245
- crate::tasks::RunOrigin::Public,
247
+ &props.workspace,
248
+ crate::tasks::RestoreMode::Existing { active },
246
249
  config,
247
- async { Ok(()) },
250
+ crate::tasks::RestoreErrors::Suppress,
248
251
  )
249
252
  .await
250
253
  {
@@ -0,0 +1,165 @@
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
+ //! A `<textarea>` whose box is sized by its own text.
14
+ //!
15
+ //! A textarea cannot grow with its content — it scrolls at whatever height
16
+ //! it was given. The technique here, shared by [`CodeEditor`] and the chat
17
+ //! prompt, is to make the textarea a purely functional layer: it takes the
18
+ //! keystrokes, the selection and the caret, but paints no glyphs
19
+ //! (`color: transparent`). Underneath sits a `<pre>` mirror of the same
20
+ //! characters which IS painted and IS in flow, so the ordinary layout
21
+ //! engine sizes the box.
22
+ //!
23
+ //! This is deliberately not a JS measure-and-resize loop
24
+ //! (`scrollHeight` → inline `height`): that re-measures on a reflow it
25
+ //! just forced, drifts whenever font-size or the loaded font changes
26
+ //! without an `input` event to re-trigger it, and jitters at fractional
27
+ //! line heights. Here the two layers share their text metrics, so growth
28
+ //! is exact by construction and free at runtime.
29
+ //!
30
+ //! Consumers decide the AXES: the mirror's `white-space` governs growth,
31
+ //! so `pre-wrap` grows only downward (the chat prompt) while `pre` also
32
+ //! grows sideways (the expression editor). They also own the box —
33
+ //! padding, borders, min/max size — with `mirrored-textarea.css` fixing
34
+ //! only what MUST agree between the two layers.
35
+ //!
36
+ //! [`CodeEditor`]: super::code_editor::CodeEditor
37
+
38
+ use yew::html::IntoPropValue;
39
+ use yew::prelude::*;
40
+
41
+ #[derive(Clone)]
42
+ pub struct Mirror(pub Html);
43
+
44
+ impl PartialEq for Mirror {
45
+ fn eq(&self, _other: &Self) -> bool {
46
+ false
47
+ }
48
+ }
49
+
50
+ impl IntoPropValue<Mirror> for Html {
51
+ fn into_prop_value(self) -> Mirror {
52
+ Mirror(self)
53
+ }
54
+ }
55
+
56
+ #[derive(Properties, PartialEq)]
57
+ pub struct MirroredTextareaProps {
58
+ /// `id` of the `<textarea>` itself — consumer CSS and tests key on it.
59
+ pub id: AttrValue,
60
+
61
+ /// The mirror's content: the same characters the textarea holds,
62
+ /// as plain text, or marked up (`CodeEditor` passes highlighted
63
+ /// spans). A stale mirror means a caret that drifts from the glyphs,
64
+ /// so this is re-rendered unconditionally — see [`Mirror`].
65
+ pub mirror: Mirror,
66
+
67
+ /// `id` of the mirror `<pre>`, for consumers that style it directly.
68
+ #[prop_or_default]
69
+ pub mirror_id: Option<AttrValue>,
70
+
71
+ /// Extra classes for the positioned container.
72
+ #[prop_or_default]
73
+ pub class: Classes,
74
+
75
+ #[prop_or_default]
76
+ pub textarea_ref: NodeRef,
77
+
78
+ #[prop_or_default]
79
+ pub mirror_ref: NodeRef,
80
+
81
+ /// The `placeholder` ATTRIBUTE, which exists for assistive tech: it
82
+ /// is painted transparent, because a CSS-`content` label cannot be
83
+ /// written into an attribute and so cannot be localized. The text a
84
+ /// sighted user reads comes from the mirror instead — see
85
+ /// [`Self::is_empty`].
86
+ #[prop_or_default]
87
+ pub placeholder: AttrValue,
88
+
89
+ /// Whether [`Self::mirror`] is empty, which the component cannot see
90
+ /// for itself (the mirror is opaque `Html`). Tags the mirror with
91
+ /// `is-empty` so a consumer's stylesheet can supply placeholder text
92
+ /// through `content`, and therefore through the intl label
93
+ /// mechanism, rather than through the untranslatable attribute.
94
+ #[prop_or_default]
95
+ pub is_empty: bool,
96
+
97
+ #[prop_or_default]
98
+ pub disabled: bool,
99
+
100
+ #[prop_or_default]
101
+ pub oninput: Callback<InputEvent>,
102
+
103
+ #[prop_or_default]
104
+ pub onkeydown: Callback<KeyboardEvent>,
105
+
106
+ #[prop_or_default]
107
+ pub onscroll: Callback<Event>,
108
+
109
+ /// Extra nodes inside the positioned container, e.g. `CodeEditor`'s
110
+ /// minimum-height sizer.
111
+ #[prop_or_default]
112
+ pub children: Children,
113
+ }
114
+
115
+ #[function_component]
116
+ pub fn MirroredTextarea(props: &MirroredTextareaProps) -> Html {
117
+ html! {
118
+ <div class={classes!("mirrored-textarea", props.class.clone())}>
119
+ // `scrollable` is the viewer's webkit scrollbar styling: the
120
+ // input layer scrolls wherever a consumer lets it (the
121
+ // expression editor syncs its `scrollTop` to the mirror and
122
+ // line numbers), and is inert where it does not (the chat
123
+ // prompt, whose wrapper scrolls instead).
124
+ <textarea
125
+ id={props.id.clone()}
126
+ ref={props.textarea_ref.clone()}
127
+ class="mirrored-textarea-input scrollable"
128
+ spellcheck="false"
129
+ placeholder={props.placeholder.clone()}
130
+ disabled={props.disabled}
131
+ oninput={props.oninput.clone()}
132
+ onkeydown={props.onkeydown.clone()}
133
+ onscroll={props.onscroll.clone()}
134
+ />
135
+ { props.children.iter().collect::<Html>() }
136
+ <pre
137
+ id={props.mirror_id.clone()}
138
+ ref={props.mirror_ref.clone()}
139
+ class={classes!(
140
+ "mirrored-textarea-mirror",
141
+ props.is_empty.then_some("is-empty"),
142
+ )}
143
+ >
144
+ { props.mirror.0.clone() }
145
+ // A trailing newline opens a line box in the textarea but
146
+ // not in the `<pre>` (which has no caret to place there),
147
+ // so the mirror would come up one line short exactly when
148
+ // the text is about to overflow. The space forces it.
149
+ { " " }
150
+ </pre>
151
+ </div>
152
+ }
153
+ }
154
+
155
+ #[cfg(test)]
156
+ mod tests {
157
+ use super::*;
158
+
159
+ #[test]
160
+ fn mirror_never_compares_equal() {
161
+ let mirror = Mirror(Html::default());
162
+ assert!(mirror != mirror.clone());
163
+ assert!(Mirror(Html::default()) != Mirror(Html::default()));
164
+ }
165
+ }
@@ -20,6 +20,7 @@ pub mod code_editor;
20
20
  pub mod color_range_selector;
21
21
  pub mod color_selector;
22
22
  pub mod debug;
23
+ pub mod mirrored_textarea;
23
24
  pub mod number_field;
24
25
  pub mod number_input;
25
26
  pub mod number_range_field;
@@ -291,6 +291,11 @@ impl Component for MainPanel {
291
291
  .link()
292
292
  .callback(|(id, x, y)| MainPanelMsg::ContextMenu(id, x, y));
293
293
  let contextmenu_listener = Closure::wrap(Box::new(move |event: web_sys::Event| {
294
+ // Shift+right-click passes through to the native browser menu.
295
+ if event.unchecked_ref::<web_sys::MouseEvent>().shift_key() {
296
+ return;
297
+ }
298
+
294
299
  let path = event.composed_path();
295
300
  let mut panel_id = None;
296
301
  for i in 0..path.length() {
@@ -14,6 +14,8 @@
14
14
  //! necessary for public Custom Elements. The rest are internal components of
15
15
  //! these 4.
16
16
 
17
+ #[cfg(feature = "llm-agent")]
18
+ pub mod chat_panel;
17
19
  pub mod column_dropdown;
18
20
  pub mod column_selector;
19
21
  pub mod column_settings_sidebar;
@@ -48,3 +50,4 @@ pub mod style;
48
50
  pub mod style_controls;
49
51
  pub mod type_icon;
50
52
  pub mod viewer;
53
+ pub mod window_editor;