@perspective-dev/viewer 4.5.1 → 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 (212) 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.cdn.d.ts +3 -0
  18. package/dist/esm/perspective-viewer.d.ts +9 -0
  19. package/dist/esm/perspective-viewer.inline.d.ts +3 -0
  20. package/dist/esm/perspective-viewer.inline.js +20 -1
  21. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  22. package/dist/esm/perspective-viewer.js +20 -1
  23. package/dist/esm/perspective-viewer.js.map +4 -4
  24. package/dist/esm/plugin.d.ts +71 -5
  25. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  26. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  27. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  28. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  29. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  30. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  31. package/dist/esm/ts-rs/OnUpdateData.d.ts +6 -0
  32. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  33. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  34. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
  35. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  36. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  37. package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +4 -0
  38. package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
  39. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  40. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  41. package/dist/wasm/perspective-viewer.d.ts +213 -79
  42. package/dist/wasm/perspective-viewer.js +537 -189
  43. package/dist/wasm/perspective-viewer.wasm +0 -0
  44. package/dist/wasm/perspective-viewer.wasm.d.ts +36 -28
  45. package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
  46. package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
  47. package/package.json +3 -2
  48. package/src/css/_column_dropdown.css +14 -0
  49. package/src/css/_context_menu.css +14 -0
  50. package/src/css/_dropdown_menu.css +14 -0
  51. package/src/css/_filter_dropdown.css +14 -0
  52. package/src/css/_function_dropdown.css +14 -0
  53. package/src/css/_viewer_bundle.css +41 -0
  54. package/src/css/column-settings-panel.css +1 -1
  55. package/src/css/containers/context-menu.css +101 -0
  56. package/src/css/panel-tab.css +193 -0
  57. package/src/css/plugin-selector.css +7 -10
  58. package/src/css/status-bar.css +75 -81
  59. package/src/css/viewer.css +181 -71
  60. package/src/rust/components/column_dropdown.rs +43 -33
  61. package/src/rust/components/column_selector/active_column.rs +7 -18
  62. package/src/rust/components/column_selector/aggregate_selector.rs +4 -10
  63. package/src/rust/components/column_selector/config_selector.rs +33 -84
  64. package/src/rust/components/column_selector/empty_column.rs +0 -3
  65. package/src/rust/components/column_selector/filter_column.rs +5 -18
  66. package/src/rust/components/column_selector/inactive_column.rs +3 -8
  67. package/src/rust/components/column_selector/invalid_column.rs +0 -4
  68. package/src/rust/components/column_selector/sort_column.rs +3 -6
  69. package/src/rust/components/column_selector.rs +5 -14
  70. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +0 -3
  71. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +0 -3
  72. package/src/rust/components/column_settings_sidebar.rs +0 -3
  73. package/src/rust/components/containers/dropdown_menu.rs +1 -3
  74. package/src/rust/components/containers/scroll_panel.rs +1 -3
  75. package/src/rust/components/containers/split_panel.rs +175 -66
  76. package/src/rust/components/containers/tab_list.rs +0 -3
  77. package/src/rust/components/context_menu.rs +130 -0
  78. package/src/rust/components/datetime_column_style.rs +0 -3
  79. package/src/rust/components/empty_row.rs +0 -3
  80. package/src/rust/components/expression_editor.rs +2 -3
  81. package/src/rust/components/filter_dropdown.rs +3 -3
  82. package/src/rust/components/font_loader.rs +2 -2
  83. package/src/rust/components/form/code_editor.rs +0 -3
  84. package/src/rust/components/form/debug.rs +3 -4
  85. package/src/rust/components/function_dropdown.rs +3 -3
  86. package/src/rust/components/global_filter_bar.rs +76 -0
  87. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  88. package/src/rust/components/main_panel/msg.rs +62 -0
  89. package/src/rust/components/main_panel/presize.rs +220 -0
  90. package/src/rust/components/main_panel/reconcile.rs +224 -0
  91. package/src/rust/components/main_panel/render.rs +385 -0
  92. package/src/rust/components/main_panel/update.rs +184 -0
  93. package/src/rust/components/main_panel.rs +311 -71
  94. package/src/rust/components/mod.rs +4 -0
  95. package/src/rust/components/number_series_style.rs +0 -3
  96. package/src/rust/components/panel_menu.rs +486 -0
  97. package/src/rust/components/panel_tab.rs +505 -0
  98. package/src/rust/components/plugin_selector.rs +0 -3
  99. package/src/rust/components/plugin_tab.rs +1 -6
  100. package/src/rust/components/portal.rs +5 -2
  101. package/src/rust/components/render_warning.rs +0 -3
  102. package/src/rust/components/settings_panel.rs +13 -5
  103. package/src/rust/components/status_bar.rs +113 -180
  104. package/src/rust/components/status_indicator.rs +6 -9
  105. package/src/rust/components/string_column_style.rs +0 -3
  106. package/src/rust/components/style/mod.rs +65 -27
  107. package/src/rust/components/style/style_provider.rs +51 -16
  108. package/src/rust/components/style_controls/number_string_format.rs +1 -8
  109. package/src/rust/components/type_icon.rs +1 -4
  110. package/src/rust/components/viewer/filters.rs +133 -0
  111. package/src/rust/components/viewer/msg.rs +180 -0
  112. package/src/rust/components/viewer/panels.rs +326 -0
  113. package/src/rust/components/viewer/render.rs +243 -0
  114. package/src/rust/components/viewer/settings.rs +402 -0
  115. package/src/rust/components/viewer/snapshots.rs +134 -0
  116. package/src/rust/components/viewer/wiring.rs +505 -0
  117. package/src/rust/components/viewer.rs +243 -758
  118. package/src/rust/config/export_method.rs +1 -1
  119. package/src/rust/config/mod.rs +4 -0
  120. package/src/rust/config/options.rs +71 -0
  121. package/src/rust/config/viewer_config.rs +24 -2
  122. package/src/rust/config/workspace_config.rs +92 -0
  123. package/src/rust/custom_elements/copy_dropdown.rs +9 -3
  124. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  125. package/src/rust/custom_elements/export_dropdown.rs +9 -3
  126. package/src/rust/custom_elements/viewer.rs +1015 -286
  127. package/src/rust/custom_events.rs +212 -61
  128. package/src/rust/js/mod.rs +2 -0
  129. package/src/rust/js/plugin.rs +297 -10
  130. package/src/rust/js/regular_layout.rs +258 -0
  131. package/src/rust/lib.rs +20 -2
  132. package/src/rust/presentation.rs +66 -33
  133. package/src/rust/queries/get_viewer_config.rs +29 -11
  134. package/src/rust/queries/plugin_column_styles.rs +2 -2
  135. package/src/rust/renderer/activate.rs +41 -14
  136. package/src/rust/renderer/dispatch.rs +436 -0
  137. package/src/rust/renderer/plugin_config.rs +495 -0
  138. package/src/rust/renderer/registry.rs +18 -6
  139. package/src/rust/renderer/render_timer.rs +29 -10
  140. package/src/rust/renderer.rs +350 -827
  141. package/src/rust/session/column_defaults_update.rs +26 -8
  142. package/src/rust/session/metadata.rs +11 -1
  143. package/src/rust/session/view_subscription.rs +30 -15
  144. package/src/rust/session.rs +753 -251
  145. package/src/rust/tasks/apply_global_filters.rs +89 -0
  146. package/src/rust/tasks/auto_pause.rs +171 -0
  147. package/src/rust/tasks/copy_export.rs +29 -19
  148. package/src/rust/tasks/create_panel.rs +243 -0
  149. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  150. package/src/rust/tasks/edit_expression.rs +4 -4
  151. package/src/rust/tasks/eject.rs +64 -23
  152. package/src/rust/tasks/mod.rs +28 -4
  153. package/src/rust/tasks/pipeline.rs +411 -0
  154. package/src/rust/tasks/presize_panels.rs +324 -0
  155. package/src/rust/tasks/reset_all.rs +22 -12
  156. package/src/rust/tasks/resize_observer.rs +124 -87
  157. package/src/rust/tasks/restore_and_render.rs +80 -86
  158. package/src/rust/tasks/restore_panel.rs +145 -0
  159. package/src/rust/tasks/send_column_config.rs +5 -2
  160. package/src/rust/tasks/send_plugin_config.rs +5 -2
  161. package/src/rust/tasks/set_edit_mode.rs +59 -0
  162. package/src/rust/tasks/sync_update_panels.rs +152 -0
  163. package/src/rust/tasks/table_lifecycle.rs +158 -0
  164. package/src/rust/tasks/update_theme.rs +56 -12
  165. package/src/rust/utils/completion.rs +56 -0
  166. package/src/rust/utils/custom_element.rs +5 -0
  167. package/src/rust/utils/debounce.rs +130 -7
  168. package/src/rust/utils/mod.rs +6 -0
  169. package/src/rust/utils/pending_effects.rs +62 -0
  170. package/src/rust/{components/style/local_style.rs → utils/spawn.rs} +17 -19
  171. package/src/rust/utils/weak_scope.rs +0 -10
  172. package/src/rust/workspace.rs +846 -0
  173. package/src/svg/active-indicator.svg +3 -0
  174. package/src/themes/botanical.css +7 -2
  175. package/src/themes/defaults.css +54 -2
  176. package/src/themes/dracula.css +8 -3
  177. package/src/themes/gruvbox-dark.css +7 -2
  178. package/src/themes/gruvbox.css +5 -2
  179. package/src/themes/icons.css +4 -2
  180. package/src/themes/monokai.css +7 -2
  181. package/src/themes/phosphor.css +7 -2
  182. package/src/themes/pro-dark.css +7 -2
  183. package/src/themes/pro.css +2 -1
  184. package/src/themes/solarized-dark.css +5 -2
  185. package/src/themes/solarized.css +5 -2
  186. package/src/themes/vaporwave.css +7 -2
  187. package/src/ts/bootstrap.ts +8 -7
  188. package/src/ts/extensions.ts +30 -2
  189. package/src/ts/perspective-viewer.ts +10 -0
  190. package/src/ts/plugin.ts +72 -5
  191. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  192. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  193. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  194. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  195. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  196. package/src/ts/ts-rs/Layout.ts +9 -0
  197. package/src/ts/ts-rs/Orientation.ts +6 -0
  198. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  199. package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
  200. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  201. package/src/ts/ts-rs/TabLayout.ts +11 -0
  202. package/src/ts/ts-rs/ViewerConfig.ts +6 -2
  203. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  204. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  205. package/tsconfig.json +1 -1
  206. package/src/rust/components/style/style_cache.rs +0 -136
  207. package/src/rust/tasks/intersection_observer.rs +0 -98
  208. package/src/rust/tasks/update_and_render.rs +0 -110
  209. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
  210. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
  211. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
  212. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
@@ -19,9 +19,6 @@ use web_sys::HtmlElement;
19
19
  use yew::html::Scope;
20
20
  use yew::prelude::*;
21
21
 
22
- use crate::components::style::LocalStyle;
23
- use crate::css;
24
-
25
22
  #[derive(Properties, Default)]
26
23
  pub struct SplitPanelProps {
27
24
  pub children: Children,
@@ -32,8 +29,10 @@ pub struct SplitPanelProps {
32
29
  #[prop_or_default]
33
30
  pub orientation: Orientation,
34
31
 
35
- /// Whether to render `<></>` empty templates as empty child panels, or
36
- /// 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.
37
36
  #[prop_or_default]
38
37
  pub skip_empty: bool,
39
38
 
@@ -56,6 +55,50 @@ pub struct SplitPanelProps {
56
55
 
57
56
  #[prop_or_default]
58
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
+ }
59
102
  }
60
103
 
61
104
  impl SplitPanelProps {
@@ -70,6 +113,8 @@ impl PartialEq for SplitPanelProps {
70
113
  && self.children == other.children
71
114
  && self.orientation == other.orientation
72
115
  && self.reverse == other.reverse
116
+ && self.size == other.size
117
+ && self.deferred == other.deferred
73
118
  }
74
119
  }
75
120
 
@@ -114,15 +159,8 @@ impl Component for SplitPanel {
114
159
  let refs = Vec::from_iter(std::iter::repeat_with(Default::default).take(len));
115
160
 
116
161
  let mut styles = vec![Default::default(); len];
117
- if let Some(x) = &ctx.props().initial_size {
118
- styles[0] = Some(match ctx.props().orientation {
119
- Orientation::Horizontal => {
120
- format!("max-width:{x}px;min-width:{x}px;width:{x}px")
121
- },
122
- Orientation::Vertical => {
123
- format!("max-height:{x}px;min-height:{x}px;height:{x}px")
124
- },
125
- });
162
+ if let Some(x) = ctx.props().size.or(ctx.props().initial_size) {
163
+ styles[0] = Some(size_style(ctx.props().orientation, x));
126
164
  }
127
165
 
128
166
  Self {
@@ -158,7 +196,12 @@ impl Component for SplitPanel {
158
196
  cb.emit(state.get_dimensions(client_offset));
159
197
  }
160
198
 
161
- 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
+ }
162
205
  }
163
206
  },
164
207
  };
@@ -176,19 +219,91 @@ impl Component for SplitPanel {
176
219
  let new_len = ctx.props().children.len();
177
220
  self.refs.resize_with(new_len, Default::default);
178
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
+
179
246
  true
180
247
  }
181
248
 
182
249
  fn view(&self, ctx: &Context<Self>) -> Html {
183
- 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
184
263
  .props()
185
264
  .children
186
265
  .iter()
187
- .filter(|x| !ctx.props().skip_empty || x != &html! { <></> })
188
266
  .enumerate()
267
+ .filter(|(_, x)| !skip_empty || !is_empty_html(x))
189
268
  .collect::<Vec<_>>();
190
269
 
191
- 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
+
192
307
  let mut classes = classes!("split-panel");
193
308
  if orientation == Orientation::Vertical {
194
309
  classes.push("orient-vertical");
@@ -198,54 +313,7 @@ impl Component for SplitPanel {
198
313
  classes.push("orient-reverse");
199
314
  }
200
315
 
201
- let count = iter.len();
202
- let contents = html! {
203
- <>
204
- <LocalStyle href={css!("containers/split-panel")} />
205
- for (i, x) in iter {
206
- if i == 0 {
207
- if count == 1 {
208
- <key=0>
209
- {x}
210
- </>
211
- } else {
212
- <SplitPanelChild
213
- key=0
214
- style={self.styles[i].clone()}
215
- ref_={self.refs[i].clone()}
216
- >
217
- { x }
218
- </SplitPanelChild>
219
- }
220
- } else if i == count - 1 {
221
- <key={i}>
222
- <SplitPanelDivider
223
- i={i - 1}
224
- orientation={ctx.props().orientation}
225
- link={ctx.link().clone()}
226
- />
227
- { x }
228
- </>
229
- } else {
230
- <key={i}>
231
- <SplitPanelDivider
232
- i={i - 1}
233
- orientation={ctx.props().orientation}
234
- link={ctx.link().clone()}
235
- />
236
- <SplitPanelChild
237
- style={self.styles[i].clone()}
238
- ref_={self.refs[i].clone()}
239
- >
240
- { x }
241
- </SplitPanelChild>
242
- </>
243
- }
244
- }
245
- </>
246
- };
247
-
248
- // TODO consider removing this
316
+ let contents = html! { <>{ for nodes.into_iter() }</> };
249
317
  if ctx.props().no_wrap {
250
318
  html! { { contents } }
251
319
  } else {
@@ -494,3 +562,44 @@ impl ResizingState {
494
562
  Ok(self.body_style.set_property("cursor", &self.cursor)?)
495
563
  }
496
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
+ }
@@ -12,8 +12,6 @@
12
12
 
13
13
  use yew::{Callback, Children, Component, Html, Properties, classes, html};
14
14
 
15
- use crate::components::style::LocalStyle;
16
- use crate::css;
17
15
  use crate::presentation::ColumnTab;
18
16
 
19
17
  #[derive(Properties, Debug, PartialEq)]
@@ -87,7 +85,6 @@ impl<T: ColumnTab> Component for TabList<T> {
87
85
 
88
86
  html! {
89
87
  <>
90
- <LocalStyle href={css!("containers/tabs")} />
91
88
  <div id="settings_tab_bar">{ for gutter_tabs }</div>
92
89
  <div id="format-tab" class="tab-content scrollable">
93
90
  { ctx.props().children.iter().nth(self.selected_idx) }
@@ -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
+ }
@@ -23,12 +23,10 @@ use wasm_bindgen::prelude::*;
23
23
  use yew::prelude::*;
24
24
 
25
25
  use super::modal::{ModalLink, SetModalLink};
26
- use super::style::LocalStyle;
27
26
  use crate::components::datetime_column_style::custom::DatetimeStyleCustom;
28
27
  use crate::components::datetime_column_style::simple::DatetimeStyleSimple;
29
28
  use crate::components::form::select_value_field::SelectValueField;
30
29
  use crate::config::*;
31
- use crate::css;
32
30
  use crate::utils::WeakScope;
33
31
 
34
32
  /// Format-only widget for `datetime` columns. Renders the `date_format`
@@ -126,7 +124,6 @@ impl Component for DatetimeColumnStyle {
126
124
  fn view(&self, ctx: &Context<Self>) -> Html {
127
125
  html! {
128
126
  <>
129
- <LocalStyle href={css!("column-style")} />
130
127
  <div id="column-style-container" class="datetime-column-style-container">
131
128
  if ctx.props().enable_time_config {
132
129
  <SelectValueField<String>
@@ -20,8 +20,6 @@ use web_sys::*;
20
20
  use yew::prelude::*;
21
21
 
22
22
  use crate::components::filter_dropdown::FilterDropDownElement;
23
- use crate::components::style::LocalStyle;
24
- use crate::css;
25
23
 
26
24
  #[derive(Properties, Derivative)]
27
25
  #[derivative(Debug)]
@@ -87,7 +85,6 @@ impl Component for EmptyRow {
87
85
 
88
86
  html! {
89
87
  <div class="pivot-column column-empty">
90
- <LocalStyle href={css!("empty-column")} />
91
88
  <input
92
89
  spellcheck="false"
93
90
  ref={self.input_ref.clone()}
@@ -16,10 +16,8 @@ use perspective_client::{ExprValidationError, clone};
16
16
  use yew::prelude::*;
17
17
 
18
18
  use super::form::code_editor::*;
19
- use super::style::LocalStyle;
20
19
  use crate::session::{Session, SessionMetadata, SessionMetadataRc};
21
20
  use crate::tasks::{ExprValidation, validate_expression};
22
- use crate::*;
23
21
 
24
22
  #[derive(Properties, PartialEq, Clone)]
25
23
  pub struct ExpressionEditorProps {
@@ -54,10 +52,12 @@ pub struct ExpressionEditor {
54
52
  expr: Rc<String>,
55
53
  error: Option<ExprValidationError>,
56
54
  oninput: Callback<Rc<String>>,
55
+
57
56
  /// Monotonically increasing request id used to drop stale
58
57
  /// validation results when the user types faster than the engine
59
58
  /// can validate.
60
59
  validation_req_id: u64,
60
+
61
61
  /// The id of the most recently dispatched validation; the result
62
62
  /// is only applied when its echoed id matches.
63
63
  last_dispatched_req_id: u64,
@@ -131,7 +131,6 @@ impl Component for ExpressionEditor {
131
131
  clone!(ctx.props().disabled);
132
132
  html! {
133
133
  <>
134
- <LocalStyle href={css!("expression-editor")} />
135
134
  <label class="item_title">{ "Expression" }</label>
136
135
  <div id="editor-container" class={disabled_class}>
137
136
  <CodeEditor
@@ -20,12 +20,11 @@ use yew::html::ImplicitClone;
20
20
  use yew::prelude::*;
21
21
 
22
22
  use super::portal::PortalModal;
23
+ use super::style::StyleSurface;
23
24
  use crate::session::Session;
24
25
  use crate::utils::*;
25
26
  use crate::*;
26
27
 
27
- static CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/css/filter-dropdown.css"));
28
-
29
28
  #[derive(Default)]
30
29
  struct FilterDropDownState {
31
30
  values: Vec<String>,
@@ -220,6 +219,7 @@ impl Component for FilterDropDownPortal {
220
219
  html! {
221
220
  <PortalModal
222
221
  tag_name="perspective-dropdown"
222
+ surface={StyleSurface::FilterDropdown}
223
223
  {target}
224
224
  own_focus=false
225
225
  {on_close}
@@ -267,7 +267,7 @@ fn FilterDropDownView(props: &FilterDropDownViewProps) -> Html {
267
267
  }
268
268
  };
269
269
 
270
- html! { <><style>{ CSS }</style>{ body }</> }
270
+ html! { <>{ body }</> }
271
271
  }
272
272
 
273
273
  fn filter_values(
@@ -154,12 +154,12 @@ impl FontLoaderProps {
154
154
  }
155
155
  }
156
156
 
157
- let fut = async { select_all(block_fonts.into_iter()).await.0 };
157
+ let fut = async { select_all(block_fonts).await.0 };
158
158
  block_promises.push(ApiFuture::new(fut))
159
159
  }
160
160
 
161
161
  if block_promises.len() != preload_fonts.len() {
162
- web_sys::console::warn_1(&format!("Missing preload fonts {:?}", &preload_fonts).into());
162
+ web_sys::console::warn_1(&format!("Missing preload fonts {:?}", preload_fonts).into());
163
163
  }
164
164
 
165
165
  let res = join_all(block_promises)
@@ -19,8 +19,6 @@ use yew::prelude::*;
19
19
 
20
20
  use crate::components::form::highlight::highlight;
21
21
  use crate::components::function_dropdown::{FunctionDropDownElement, FunctionDropDownPortal};
22
- use crate::components::style::LocalStyle;
23
- use crate::css;
24
22
  use crate::exprtk::{Cursor, tokenize};
25
23
  use crate::utils::*;
26
24
 
@@ -174,7 +172,6 @@ pub fn code_editor(props: &CodeEditorProps) -> Html {
174
172
  clone!(props.disabled);
175
173
  html! {
176
174
  <>
177
- <LocalStyle href={css!("form/code-editor")} />
178
175
  <div id="editor" {class}>
179
176
  <div id="line_numbers" ref={lineno_ref}>{ line_numbers }</div>
180
177
  <div id="editor-inner" {class}>
@@ -18,8 +18,6 @@ use wasm_bindgen::prelude::*;
18
18
  use yew::prelude::*;
19
19
 
20
20
  use crate::components::form::code_editor::CodeEditor;
21
- use crate::components::style::LocalStyle;
22
- use crate::css;
23
21
  use crate::js::{MimeType, copy_to_clipboard, paste_from_clipboard};
24
22
  use crate::presentation::*;
25
23
  use crate::renderer::*;
@@ -166,8 +164,6 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
166
164
 
167
165
  html! {
168
166
  <>
169
- <LocalStyle href={css!("containers/tabs")} />
170
- <LocalStyle href={css!("form/debug")} />
171
167
  <div id="debug-panel-overflow">
172
168
  <div id="debug-panel" class="sidebar_column" ref={sizer}>
173
169
  <div id="debug-panel-controls">
@@ -244,6 +240,9 @@ impl DebugPanelProps {
244
240
  &props.session,
245
241
  &props.renderer,
246
242
  &props.presentation,
243
+ // A user-pasted config apply — an explicit request,
244
+ // same affordance as the public `restore()`.
245
+ crate::tasks::RunOrigin::Public,
247
246
  config,
248
247
  async { Ok(()) },
249
248
  )
@@ -20,10 +20,9 @@ use yew::html::ImplicitClone;
20
20
  use yew::prelude::*;
21
21
 
22
22
  use super::portal::PortalModal;
23
+ use super::style::StyleSurface;
23
24
  use crate::utils::*;
24
25
 
25
- static CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/css/function-dropdown.css"));
26
-
27
26
  #[derive(Default)]
28
27
  struct FunctionDropDownState {
29
28
  values: Vec<CompletionItemSuggestion>,
@@ -158,6 +157,7 @@ impl Component for FunctionDropDownPortal {
158
157
  html! {
159
158
  <PortalModal
160
159
  tag_name="perspective-dropdown"
160
+ surface={StyleSurface::FunctionDropdown}
161
161
  {target}
162
162
  own_focus=false
163
163
  {on_close}
@@ -211,7 +211,7 @@ fn FunctionDropDownView(props: &FunctionDropDownViewProps) -> Html {
211
211
  }
212
212
  };
213
213
 
214
- html! { <><style>{ CSS }</style>{ body }</> }
214
+ html! { <>{ body }</> }
215
215
  }
216
216
 
217
217
  fn filter_values(input: &str) -> Vec<CompletionItemSuggestion> {