@perspective-dev/viewer 4.5.2 → 5.0.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 (178) hide show
  1. package/dist/cdn/perspective-viewer.js +20 -1
  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/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/esm/extensions.d.ts +22 -3
  17. package/dist/esm/perspective-viewer.d.ts +9 -0
  18. package/dist/esm/perspective-viewer.inline.js +20 -1
  19. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  20. package/dist/esm/perspective-viewer.js +20 -1
  21. package/dist/esm/perspective-viewer.js.map +4 -4
  22. package/dist/esm/plugin.d.ts +71 -5
  23. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  24. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  25. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  26. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  27. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  28. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  29. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  30. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  31. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
  32. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  33. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  34. package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
  35. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  36. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  37. package/dist/wasm/perspective-viewer.d.ts +211 -77
  38. package/dist/wasm/perspective-viewer.js +528 -180
  39. package/dist/wasm/perspective-viewer.wasm +0 -0
  40. package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
  41. package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
  42. package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
  43. package/package.json +3 -2
  44. package/src/css/_context_menu.css +14 -0
  45. package/src/css/column-settings-panel.css +1 -1
  46. package/src/css/containers/context-menu.css +101 -0
  47. package/src/css/panel-tab.css +193 -0
  48. package/src/css/plugin-selector.css +7 -10
  49. package/src/css/status-bar.css +75 -81
  50. package/src/css/viewer.css +180 -70
  51. package/src/rust/components/column_dropdown.rs +40 -30
  52. package/src/rust/components/column_selector/active_column.rs +7 -18
  53. package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
  54. package/src/rust/components/column_selector/config_selector.rs +33 -81
  55. package/src/rust/components/column_selector/filter_column.rs +5 -15
  56. package/src/rust/components/column_selector/inactive_column.rs +3 -8
  57. package/src/rust/components/column_selector/sort_column.rs +3 -6
  58. package/src/rust/components/column_selector.rs +5 -11
  59. package/src/rust/components/containers/split_panel.rs +175 -62
  60. package/src/rust/components/context_menu.rs +130 -0
  61. package/src/rust/components/form/debug.rs +3 -0
  62. package/src/rust/components/global_filter_bar.rs +76 -0
  63. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  64. package/src/rust/components/main_panel/msg.rs +62 -0
  65. package/src/rust/components/main_panel/presize.rs +220 -0
  66. package/src/rust/components/main_panel/reconcile.rs +224 -0
  67. package/src/rust/components/main_panel/render.rs +385 -0
  68. package/src/rust/components/main_panel/update.rs +184 -0
  69. package/src/rust/components/main_panel.rs +311 -71
  70. package/src/rust/components/mod.rs +4 -0
  71. package/src/rust/components/panel_menu.rs +486 -0
  72. package/src/rust/components/panel_tab.rs +505 -0
  73. package/src/rust/components/plugin_tab.rs +1 -1
  74. package/src/rust/components/settings_panel.rs +13 -5
  75. package/src/rust/components/status_bar.rs +110 -178
  76. package/src/rust/components/status_indicator.rs +6 -9
  77. package/src/rust/components/style/mod.rs +5 -0
  78. package/src/rust/components/style/style_provider.rs +2 -0
  79. package/src/rust/components/viewer/filters.rs +133 -0
  80. package/src/rust/components/viewer/msg.rs +180 -0
  81. package/src/rust/components/viewer/panels.rs +326 -0
  82. package/src/rust/components/viewer/render.rs +243 -0
  83. package/src/rust/components/viewer/settings.rs +402 -0
  84. package/src/rust/components/viewer/snapshots.rs +134 -0
  85. package/src/rust/components/viewer/wiring.rs +505 -0
  86. package/src/rust/components/viewer.rs +243 -756
  87. package/src/rust/config/export_method.rs +1 -1
  88. package/src/rust/config/mod.rs +4 -0
  89. package/src/rust/config/options.rs +71 -0
  90. package/src/rust/config/viewer_config.rs +24 -2
  91. package/src/rust/config/workspace_config.rs +92 -0
  92. package/src/rust/custom_elements/copy_dropdown.rs +3 -1
  93. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  94. package/src/rust/custom_elements/export_dropdown.rs +3 -1
  95. package/src/rust/custom_elements/viewer.rs +1015 -286
  96. package/src/rust/custom_events.rs +212 -61
  97. package/src/rust/js/mod.rs +2 -0
  98. package/src/rust/js/plugin.rs +297 -10
  99. package/src/rust/js/regular_layout.rs +258 -0
  100. package/src/rust/lib.rs +20 -1
  101. package/src/rust/presentation.rs +66 -33
  102. package/src/rust/queries/get_viewer_config.rs +29 -11
  103. package/src/rust/queries/plugin_column_styles.rs +2 -2
  104. package/src/rust/renderer/activate.rs +41 -14
  105. package/src/rust/renderer/dispatch.rs +436 -0
  106. package/src/rust/renderer/plugin_config.rs +495 -0
  107. package/src/rust/renderer/registry.rs +18 -6
  108. package/src/rust/renderer/render_timer.rs +29 -10
  109. package/src/rust/renderer.rs +350 -837
  110. package/src/rust/session/column_defaults_update.rs +26 -8
  111. package/src/rust/session/view_subscription.rs +30 -15
  112. package/src/rust/session.rs +753 -264
  113. package/src/rust/tasks/apply_global_filters.rs +89 -0
  114. package/src/rust/tasks/auto_pause.rs +171 -0
  115. package/src/rust/tasks/copy_export.rs +29 -19
  116. package/src/rust/tasks/create_panel.rs +243 -0
  117. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  118. package/src/rust/tasks/edit_expression.rs +4 -4
  119. package/src/rust/tasks/eject.rs +64 -23
  120. package/src/rust/tasks/mod.rs +28 -4
  121. package/src/rust/tasks/pipeline.rs +411 -0
  122. package/src/rust/tasks/presize_panels.rs +324 -0
  123. package/src/rust/tasks/reset_all.rs +22 -12
  124. package/src/rust/tasks/resize_observer.rs +124 -87
  125. package/src/rust/tasks/restore_and_render.rs +80 -86
  126. package/src/rust/tasks/restore_panel.rs +145 -0
  127. package/src/rust/tasks/send_column_config.rs +5 -2
  128. package/src/rust/tasks/send_plugin_config.rs +5 -2
  129. package/src/rust/tasks/set_edit_mode.rs +59 -0
  130. package/src/rust/tasks/sync_update_panels.rs +152 -0
  131. package/src/rust/tasks/table_lifecycle.rs +158 -0
  132. package/src/rust/tasks/update_theme.rs +56 -12
  133. package/src/rust/utils/completion.rs +56 -0
  134. package/src/rust/utils/custom_element.rs +5 -0
  135. package/src/rust/utils/debounce.rs +130 -7
  136. package/src/rust/utils/mod.rs +6 -0
  137. package/src/rust/utils/pending_effects.rs +62 -0
  138. package/src/rust/utils/spawn.rs +33 -0
  139. package/src/rust/utils/weak_scope.rs +0 -10
  140. package/src/rust/workspace.rs +846 -0
  141. package/src/svg/active-indicator.svg +3 -0
  142. package/src/themes/botanical.css +5 -2
  143. package/src/themes/defaults.css +54 -2
  144. package/src/themes/dracula.css +6 -3
  145. package/src/themes/gruvbox-dark.css +5 -2
  146. package/src/themes/gruvbox.css +5 -2
  147. package/src/themes/icons.css +4 -2
  148. package/src/themes/monokai.css +5 -2
  149. package/src/themes/phosphor.css +5 -2
  150. package/src/themes/pro-dark.css +5 -2
  151. package/src/themes/pro.css +2 -1
  152. package/src/themes/solarized-dark.css +5 -2
  153. package/src/themes/solarized.css +5 -2
  154. package/src/themes/vaporwave.css +5 -2
  155. package/src/ts/bootstrap.ts +8 -7
  156. package/src/ts/extensions.ts +30 -2
  157. package/src/ts/perspective-viewer.ts +10 -0
  158. package/src/ts/plugin.ts +72 -5
  159. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  160. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  161. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  162. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  163. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  164. package/src/ts/ts-rs/Layout.ts +9 -0
  165. package/src/ts/ts-rs/Orientation.ts +6 -0
  166. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  167. package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
  168. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  169. package/src/ts/ts-rs/TabLayout.ts +11 -0
  170. package/src/ts/ts-rs/ViewerConfig.ts +6 -2
  171. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  172. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  173. package/src/rust/tasks/intersection_observer.rs +0 -98
  174. package/src/rust/tasks/update_and_render.rs +0 -110
  175. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
  176. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
  177. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
  178. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
@@ -29,8 +29,10 @@ pub struct SplitPanelProps {
29
29
  #[prop_or_default]
30
30
  pub orientation: Orientation,
31
31
 
32
- /// Whether to render `<></>` empty templates as empty child panels, or
33
- /// omit them entirely.
32
+ /// Whether to OMIT children that render nothing (an empty fragment, or
33
+ /// any nesting of empty fragments — see [`is_empty_html`]) instead of
34
+ /// rendering them as empty panes. A skipped pane takes its divider with
35
+ /// it.
34
36
  #[prop_or_default]
35
37
  pub skip_empty: bool,
36
38
 
@@ -53,6 +55,50 @@ pub struct SplitPanelProps {
53
55
 
54
56
  #[prop_or_default]
55
57
  pub initial_size: Option<i32>,
58
+
59
+ /// When `true`, a divider drag does NOT apply pane sizes directly:
60
+ /// `MoveResizing` only emits `on_resize` with the proposed dims, and pane
61
+ /// 0's committed size is driven exclusively by the controlled
62
+ /// [`Self::size`] prop — set by the parent when it is ready (e.g. after
63
+ /// pre-rendering panel content at the target size). This is the presize
64
+ /// gate for continuous divider drags (`PRESIZE_EVERYWHERE_PLAN.md`,
65
+ /// P1): geometry never outruns content. Only meaningful when the
66
+ /// resizable pane is child 0 (as in the viewer's `app_panel`).
67
+ #[prop_or_default]
68
+ pub deferred: bool,
69
+
70
+ /// Controlled committed size for pane 0 (deferred mode). Unlike
71
+ /// [`Self::initial_size`] (read once at `create`), changes to this prop
72
+ /// re-derive pane 0's style on every `changed`.
73
+ #[prop_or_default]
74
+ pub size: Option<i32>,
75
+ }
76
+
77
+ /// `true` when `node` renders NOTHING: an empty fragment, or a fragment of
78
+ /// nothing but empty fragments. SEMANTIC, not literal — the `html!`
79
+ /// if-syntax (and other wrappers) nests an empty branch inside another
80
+ /// `VList` rather than yielding a bare `<></>`, which is why a literal
81
+ /// `x != html! { <></> }` comparison is not a usable emptiness test (see
82
+ /// the mechanism unit test). Deliberately conservative: `VText("")` and
83
+ /// other node kinds are NOT considered empty.
84
+ fn is_empty_html(node: &Html) -> bool {
85
+ match node {
86
+ Html::VList(list) => list.iter().all(is_empty_html),
87
+ _ => false,
88
+ }
89
+ }
90
+
91
+ /// The fixed-size pane style for a committed size (the complement pane
92
+ /// flex-fills).
93
+ fn size_style(orientation: Orientation, x: i32) -> String {
94
+ match orientation {
95
+ Orientation::Horizontal => {
96
+ format!("max-width:{x}px;min-width:{x}px;width:{x}px")
97
+ },
98
+ Orientation::Vertical => {
99
+ format!("max-height:{x}px;min-height:{x}px;height:{x}px")
100
+ },
101
+ }
56
102
  }
57
103
 
58
104
  impl SplitPanelProps {
@@ -67,6 +113,8 @@ impl PartialEq for SplitPanelProps {
67
113
  && self.children == other.children
68
114
  && self.orientation == other.orientation
69
115
  && self.reverse == other.reverse
116
+ && self.size == other.size
117
+ && self.deferred == other.deferred
70
118
  }
71
119
  }
72
120
 
@@ -111,15 +159,8 @@ impl Component for SplitPanel {
111
159
  let refs = Vec::from_iter(std::iter::repeat_with(Default::default).take(len));
112
160
 
113
161
  let mut styles = vec![Default::default(); len];
114
- if let Some(x) = &ctx.props().initial_size {
115
- styles[0] = Some(match ctx.props().orientation {
116
- Orientation::Horizontal => {
117
- format!("max-width:{x}px;min-width:{x}px;width:{x}px")
118
- },
119
- Orientation::Vertical => {
120
- format!("max-height:{x}px;min-height:{x}px;height:{x}px")
121
- },
122
- });
162
+ if let Some(x) = ctx.props().size.or(ctx.props().initial_size) {
163
+ styles[0] = Some(size_style(ctx.props().orientation, x));
123
164
  }
124
165
 
125
166
  Self {
@@ -155,7 +196,12 @@ impl Component for SplitPanel {
155
196
  cb.emit(state.get_dimensions(client_offset));
156
197
  }
157
198
 
158
- self.styles[state.index] = state.get_style(client_offset);
199
+ // Deferred mode: propose only — the committed size arrives
200
+ // back through the controlled `size` prop once the parent
201
+ // has pre-rendered content at the target.
202
+ if !ctx.props().deferred {
203
+ self.styles[state.index] = state.get_style(client_offset);
204
+ }
159
205
  }
160
206
  },
161
207
  };
@@ -173,19 +219,91 @@ impl Component for SplitPanel {
173
219
  let new_len = ctx.props().children.len();
174
220
  self.refs.resize_with(new_len, Default::default);
175
221
  self.styles.resize(new_len, Default::default());
222
+
223
+ if let Some(state) = self.resize_state.as_ref() {
224
+ let skip_empty = ctx.props().skip_empty;
225
+ let still_visible = ctx
226
+ .props()
227
+ .children
228
+ .iter()
229
+ .enumerate()
230
+ .any(|(i, x)| i == state.index && (!skip_empty || !is_empty_html(&x)));
231
+
232
+ if !still_visible {
233
+ self.resize_state = None;
234
+ }
235
+ }
236
+
237
+ // Deferred mode: pane 0's style tracks the controlled `size` prop
238
+ // (`None` = natural width, e.g. after a divider double-click reset).
239
+ if ctx.props().deferred {
240
+ self.styles[0] = ctx
241
+ .props()
242
+ .size
243
+ .map(|x| size_style(ctx.props().orientation, x));
244
+ }
245
+
176
246
  true
177
247
  }
178
248
 
179
249
  fn view(&self, ctx: &Context<Self>) -> Html {
180
- let iter = ctx
250
+ let skip_empty = ctx.props().skip_empty;
251
+ let orientation = ctx.props().orientation;
252
+
253
+ // Pair each *visible* pane with its ORIGINAL child index and key every
254
+ // pane (and divider) by that stable identity — not its post-filter
255
+ // position. Toggling one pane in/out (e.g. the settings sidebar) then
256
+ // never shifts a sibling's key, so Yew reconciles the survivor in place
257
+ // rather than remounting it. A `MainPanel` remount here would tear down
258
+ // and recreate the embedded `<regular-layout>` and the `<slot>`s
259
+ // projecting the plugins — the bug this avoids. Dividers are independent
260
+ // keyed nodes for the same reason: a divider appearing before a pane must
261
+ // not alter that pane's own keyed subtree.
262
+ let panes = ctx
181
263
  .props()
182
264
  .children
183
265
  .iter()
184
- .filter(|x| !ctx.props().skip_empty || x != &html! { <></> })
185
266
  .enumerate()
267
+ .filter(|(_, x)| !skip_empty || !is_empty_html(x))
186
268
  .collect::<Vec<_>>();
187
269
 
188
- let orientation = ctx.props().orientation;
270
+ let last = panes.len().saturating_sub(1);
271
+ let mut nodes: Vec<Html> = Vec::with_capacity(panes.len() * 2);
272
+ let mut prev: Option<usize> = None;
273
+ for (pos, (i, x)) in panes.into_iter().enumerate() {
274
+ // A divider precedes every visible pane except the first; it resizes
275
+ // the *previous* visible pane (`prev`, an original index).
276
+ if let Some(p) = prev {
277
+ nodes.push(html! {
278
+ <SplitPanelDivider
279
+ key={format!("divider-{i}")}
280
+ i={p}
281
+ {orientation}
282
+ link={ctx.link().clone()}
283
+ />
284
+ });
285
+ }
286
+
287
+ // The last visible pane flex-fills (rendered bare, no width
288
+ // override); earlier panes are `SplitPanelChild`s that can carry a
289
+ // dragged size.
290
+ nodes.push(if pos == last {
291
+ html! { <key={i}>{ x }</> }
292
+ } else {
293
+ html! {
294
+ <SplitPanelChild
295
+ key={i}
296
+ style={self.styles[i].clone()}
297
+ ref_={self.refs[i].clone()}
298
+ >
299
+ { x }
300
+ </SplitPanelChild>
301
+ }
302
+ });
303
+
304
+ prev = Some(i);
305
+ }
306
+
189
307
  let mut classes = classes!("split-panel");
190
308
  if orientation == Orientation::Vertical {
191
309
  classes.push("orient-vertical");
@@ -195,53 +313,7 @@ impl Component for SplitPanel {
195
313
  classes.push("orient-reverse");
196
314
  }
197
315
 
198
- let count = iter.len();
199
- let contents = html! {
200
- <>
201
- for (i, x) in iter {
202
- if i == 0 {
203
- if count == 1 {
204
- <key=0>
205
- {x}
206
- </>
207
- } else {
208
- <SplitPanelChild
209
- key=0
210
- style={self.styles[i].clone()}
211
- ref_={self.refs[i].clone()}
212
- >
213
- { x }
214
- </SplitPanelChild>
215
- }
216
- } else if i == count - 1 {
217
- <key={i}>
218
- <SplitPanelDivider
219
- i={i - 1}
220
- orientation={ctx.props().orientation}
221
- link={ctx.link().clone()}
222
- />
223
- { x }
224
- </>
225
- } else {
226
- <key={i}>
227
- <SplitPanelDivider
228
- i={i - 1}
229
- orientation={ctx.props().orientation}
230
- link={ctx.link().clone()}
231
- />
232
- <SplitPanelChild
233
- style={self.styles[i].clone()}
234
- ref_={self.refs[i].clone()}
235
- >
236
- { x }
237
- </SplitPanelChild>
238
- </>
239
- }
240
- }
241
- </>
242
- };
243
-
244
- // TODO consider removing this
316
+ let contents = html! { <>{ for nodes.into_iter() }</> };
245
317
  if ctx.props().no_wrap {
246
318
  html! { { contents } }
247
319
  } else {
@@ -490,3 +562,44 @@ impl ResizingState {
490
562
  Ok(self.body_style.set_property("cursor", &self.cursor)?)
491
563
  }
492
564
  }
565
+
566
+ #[cfg(test)]
567
+ mod tests {
568
+ use super::*;
569
+
570
+ #[test]
571
+ fn html_if_empty_branch_is_not_literally_empty() {
572
+ let via_if: Html = html! { if false { <div /> } else { <></> } };
573
+ assert_ne!(via_if, html! { <></> });
574
+ }
575
+
576
+ #[test]
577
+ fn empty_fragment_is_empty() {
578
+ assert!(is_empty_html(&html! { <></> }));
579
+ }
580
+
581
+ #[test]
582
+ fn html_if_empty_branch_is_semantically_empty() {
583
+ assert!(is_empty_html(&html! { if false { <div/> } else { <></> } }));
584
+ }
585
+
586
+ #[test]
587
+ fn nested_empty_fragments_are_empty() {
588
+ assert!(is_empty_html(&html! { <><><></></></> }));
589
+ }
590
+
591
+ #[test]
592
+ fn tag_is_not_empty() {
593
+ assert!(!is_empty_html(&html! { <div/> }));
594
+ }
595
+
596
+ #[test]
597
+ fn fragment_containing_a_tag_is_not_empty() {
598
+ assert!(!is_empty_html(&html! { <><div/></> }));
599
+ }
600
+
601
+ #[test]
602
+ fn text_is_not_empty() {
603
+ assert!(!is_empty_html(&html! { { "" } }));
604
+ }
605
+ }
@@ -0,0 +1,130 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ use web_sys::MouseEvent;
14
+ use yew::prelude::*;
15
+
16
+ /// A single command row in a [`ContextMenu`].
17
+ #[derive(Clone, PartialEq)]
18
+ pub struct ContextMenuItem {
19
+ pub label: String,
20
+ pub on_select: Callback<()>,
21
+ pub disabled: bool,
22
+ }
23
+
24
+ /// A row in a [`ContextMenu`]: a plain command [`ContextMenuItem`], a
25
+ /// non-interactive group label, or a row with a hover-opened sub-menu flyout.
26
+ #[derive(Clone, PartialEq)]
27
+ pub enum ContextMenuEntry {
28
+ Item(ContextMenuItem),
29
+ Header(String),
30
+ Submenu {
31
+ label: String,
32
+
33
+ /// Click-through command for the parent row itself (`None` makes the
34
+ /// row hover-only).
35
+ on_select: Option<Callback<()>>,
36
+ entries: Vec<ContextMenuEntry>,
37
+ },
38
+ }
39
+
40
+ #[derive(Properties, PartialEq)]
41
+ pub struct ContextMenuProps {
42
+ pub entries: Vec<ContextMenuEntry>,
43
+
44
+ /// Fired after an item is selected. Outside-click dismissal is the
45
+ /// enclosing `PortalModal`'s job (blur), not this component's.
46
+ #[prop_or_default]
47
+ pub on_close: Callback<()>,
48
+ }
49
+
50
+ /// The command-item list of a panel context menu. Purely presentational — the
51
+ /// command set is supplied by the parent as plain [`Callback`]s, and
52
+ /// positioning/dismissal/theming belong to the enclosing `PortalModal` (the
53
+ /// same body-mounted, `theme`-attributed delivery as the Export/Copy menus).
54
+ /// Sub-menus are CSS-only `:hover` flyouts nested inside their parent row, so
55
+ /// they live inside the same `PortalModal` host (focus never leaves it) and
56
+ /// selection routes through the same `on_select` + `on_close` path.
57
+ pub struct ContextMenu;
58
+
59
+ impl Component for ContextMenu {
60
+ type Message = ();
61
+ type Properties = ContextMenuProps;
62
+
63
+ fn create(_ctx: &Context<Self>) -> Self {
64
+ Self
65
+ }
66
+
67
+ fn view(&self, ctx: &Context<Self>) -> Html {
68
+ let props = ctx.props();
69
+ let entries = props
70
+ .entries
71
+ .iter()
72
+ .map(|entry| render_entry(entry, &props.on_close));
73
+
74
+ html! { <div class="context-menu">{ for entries }</div> }
75
+ }
76
+ }
77
+
78
+ fn render_entry(entry: &ContextMenuEntry, on_close: &Callback<()>) -> Html {
79
+ match entry {
80
+ ContextMenuEntry::Item(item) => {
81
+ let disabled = item.disabled;
82
+ let onclick = {
83
+ let on_select = item.on_select.clone();
84
+ let on_close = on_close.clone();
85
+ Callback::from(move |e: MouseEvent| {
86
+ e.stop_propagation();
87
+ if !disabled {
88
+ on_select.emit(());
89
+ on_close.emit(());
90
+ }
91
+ })
92
+ };
93
+
94
+ let mut class = classes!("context-menu-item");
95
+ if disabled {
96
+ class.push("disabled");
97
+ }
98
+
99
+ html! { <span {class} {onclick}>{ item.label.clone() }</span> }
100
+ },
101
+ ContextMenuEntry::Header(label) => {
102
+ html! { <span class="context-menu-header">{ label.clone() }</span> }
103
+ },
104
+ ContextMenuEntry::Submenu {
105
+ label,
106
+ on_select,
107
+ entries,
108
+ } => {
109
+ let onclick = {
110
+ let on_select = on_select.clone();
111
+ let on_close = on_close.clone();
112
+ Callback::from(move |e: MouseEvent| {
113
+ e.stop_propagation();
114
+ if let Some(on_select) = &on_select {
115
+ on_select.emit(());
116
+ on_close.emit(());
117
+ }
118
+ })
119
+ };
120
+
121
+ let children = entries.iter().map(|entry| render_entry(entry, on_close));
122
+ html! {
123
+ <span class="context-menu-item has-submenu" {onclick}>
124
+ { label.clone() }
125
+ <div class="context-menu context-menu-submenu">{ for children }</div>
126
+ </span>
127
+ }
128
+ },
129
+ }
130
+ }
@@ -240,6 +240,9 @@ impl DebugPanelProps {
240
240
  &props.session,
241
241
  &props.renderer,
242
242
  &props.presentation,
243
+ // A user-pasted config apply — an explicit request,
244
+ // same affordance as the public `restore()`.
245
+ crate::tasks::RunOrigin::Public,
243
246
  config,
244
247
  async { Ok(()) },
245
248
  )
@@ -0,0 +1,76 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ use perspective_client::config::Filter;
14
+ use web_sys::MouseEvent;
15
+ use yew::prelude::*;
16
+
17
+ #[derive(Properties, PartialEq)]
18
+ pub struct GlobalFilterBarProps {
19
+ /// The element-level global filters (applied to every non-master panel).
20
+ pub filters: Vec<Filter>,
21
+
22
+ /// Fired with a filter's index to remove it.
23
+ pub on_remove: Callback<usize>,
24
+
25
+ /// Fired to clear all global filters.
26
+ pub on_clear: Callback<()>,
27
+ }
28
+
29
+ /// The global-filter bar: the element-level filters (fed by master/detail
30
+ /// selection) as removable chips, rendered inline in the `StatusBar` between
31
+ /// the row stats and the menu icons. Purely presentational — the parent owns
32
+ /// the filter set and applies it.
33
+ pub struct GlobalFilterBar;
34
+
35
+ impl Component for GlobalFilterBar {
36
+ type Message = ();
37
+ type Properties = GlobalFilterBarProps;
38
+
39
+ fn create(_ctx: &Context<Self>) -> Self {
40
+ Self
41
+ }
42
+
43
+ fn view(&self, ctx: &Context<Self>) -> Html {
44
+ let chips = ctx
45
+ .props()
46
+ .filters
47
+ .iter()
48
+ .enumerate()
49
+ .map(|(index, filter)| {
50
+ let on_remove = ctx.props().on_remove.clone();
51
+ let onclick = Callback::from(move |e: MouseEvent| {
52
+ e.stop_propagation();
53
+ on_remove.emit(index);
54
+ });
55
+
56
+ let label = format!("{} {} {}", filter.column(), filter.op(), filter.term());
57
+ html! {
58
+ <span class="global-filter-chip">
59
+ <span class="global-filter-chip-label">{ label }</span>
60
+ <span class="global-filter-chip-remove" {onclick}>{ "×" }</span>
61
+ </span>
62
+ }
63
+ });
64
+
65
+ let on_clear = ctx.props().on_clear.clone();
66
+ let onclear = Callback::from(move |_: MouseEvent| on_clear.emit(()));
67
+
68
+ html! {
69
+ <div id="global_filter_bar">
70
+ <span class="global-filter-bar-label">{ "Filter" }</span>
71
+ <span class="global-filter-bar-chips">{ for chips }</span>
72
+ <span class="global-filter-bar-clear" onclick={onclear}>{ "Clear" }</span>
73
+ </div>
74
+ }
75
+ }
76
+ }