@perspective-dev/viewer 5.3.0 → 5.4.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 (265) hide show
  1. package/dist/cdn/perspective-viewer.js +8 -8
  2. package/dist/cdn/perspective-viewer.js.map +3 -3
  3. package/dist/css/blueprint.css +1 -0
  4. package/dist/css/botanical.css +1 -1
  5. package/dist/css/dracula.css +1 -1
  6. package/dist/css/eggplant.css +1 -0
  7. package/dist/css/gruvbox-dark.css +1 -1
  8. package/dist/css/gruvbox.css +1 -1
  9. package/dist/css/icons.css +1 -1
  10. package/dist/css/intl/ar.css +1 -0
  11. package/dist/css/intl/de.css +1 -1
  12. package/dist/css/intl/es.css +1 -1
  13. package/dist/css/intl/fr.css +1 -1
  14. package/dist/css/intl/ja.css +1 -1
  15. package/dist/css/intl/pt.css +1 -1
  16. package/dist/css/intl/zh.css +1 -1
  17. package/dist/css/intl.css +1 -1
  18. package/dist/css/ledger.css +1 -0
  19. package/dist/css/monokai.css +1 -1
  20. package/dist/css/nord.css +1 -0
  21. package/dist/css/phosphor.css +1 -1
  22. package/dist/css/pro-dark.css +1 -1
  23. package/dist/css/pro.css +1 -1
  24. package/dist/css/solarized-dark.css +1 -1
  25. package/dist/css/solarized.css +1 -1
  26. package/dist/css/themes.css +1 -1
  27. package/dist/css/vaporwave.css +1 -1
  28. package/dist/css/velvet.css +1 -0
  29. package/dist/docs/perspective-docs.json +1 -1
  30. package/dist/esm/column-format.d.ts +16 -3
  31. package/dist/esm/extensions.d.ts +0 -5
  32. package/dist/esm/perspective-viewer.inline.js +7 -7
  33. package/dist/esm/perspective-viewer.inline.js.map +3 -3
  34. package/dist/esm/perspective-viewer.js +8 -8
  35. package/dist/esm/perspective-viewer.js.map +3 -3
  36. package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +1 -1
  37. package/dist/wasm/perspective-viewer.d.ts +62 -73
  38. package/dist/wasm/perspective-viewer.js +230 -238
  39. package/dist/wasm/perspective-viewer.wasm +0 -0
  40. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -28
  41. package/package.json +7 -7
  42. package/src/css/_context_menu.css +1 -1
  43. package/src/css/_dropdown_menu.css +1 -1
  44. package/src/css/_viewer_bundle.css +7 -8
  45. package/src/css/column-dropdown.css +0 -3
  46. package/src/css/column-selector.css +8 -5
  47. package/src/css/column-settings-panel.css +0 -206
  48. package/src/css/column-style.css +95 -0
  49. package/src/css/config-selector.css +18 -10
  50. package/src/css/filter-dropdown.css +5 -5
  51. package/src/css/form/debug.css +28 -4
  52. package/src/css/function-dropdown.css +5 -5
  53. package/src/css/panel-tab.css +23 -0
  54. package/src/css/plugin-selector.css +6 -5
  55. package/src/css/render-warning.css +1 -1
  56. package/src/css/status-bar.css +52 -21
  57. package/src/css/type-icon.css +1 -3
  58. package/src/css/{containers → ui/containers}/context-menu.css +22 -16
  59. package/src/css/ui/containers/dropdown-menu-items.css +66 -0
  60. package/src/css/{containers → ui/containers}/dropdown-menu.css +8 -53
  61. package/src/css/{containers → ui/containers}/tabs.css +13 -3
  62. package/src/css/{dom → ui/dom}/select.css +19 -8
  63. package/src/css/viewer.css +32 -31
  64. package/src/rust/agent/tools.rs +5 -2
  65. package/src/rust/components/chat_panel.rs +1 -1
  66. package/src/rust/components/{form/code_editor.rs → code_editor.rs} +4 -2
  67. package/src/rust/components/column_dropdown.rs +2 -2
  68. package/src/rust/components/column_selector/active_column.rs +15 -9
  69. package/src/rust/components/column_selector/aggregate_selector.rs +1 -1
  70. package/src/rust/components/column_selector/config_selector.rs +2 -2
  71. package/src/rust/components/column_selector/filter_column.rs +8 -5
  72. package/src/rust/components/column_selector/inactive_column.rs +4 -2
  73. package/src/rust/components/column_selector/pivot_column.rs +4 -3
  74. package/src/rust/components/column_selector/sort_column.rs +7 -4
  75. package/src/rust/components/column_selector.rs +3 -3
  76. package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +1 -1
  77. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +15 -17
  78. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +1 -1
  79. package/src/rust/components/column_settings_sidebar/style_tab.rs +296 -238
  80. package/src/rust/components/column_settings_sidebar.rs +3 -4
  81. package/src/rust/components/copy_dropdown.rs +1 -1
  82. package/src/rust/components/datetime_column_style/custom.rs +1 -3
  83. package/src/rust/components/datetime_column_style/simple.rs +16 -6
  84. package/src/rust/components/datetime_column_style.rs +51 -12
  85. package/src/rust/components/{form/debug.rs → debug_panel.rs} +5 -5
  86. package/src/rust/components/{containers/dragdrop_list.rs → dragdrop_list.rs} +2 -10
  87. package/src/rust/components/export_dropdown.rs +1 -1
  88. package/src/rust/components/expression_editor.rs +1 -1
  89. package/src/rust/components/filter_dropdown.rs +2 -2
  90. package/src/rust/components/function_dropdown.rs +2 -2
  91. package/src/rust/components/main_panel/frame_theme.rs +3 -59
  92. package/src/rust/components/main_panel/reconcile.rs +9 -4
  93. package/src/rust/components/main_panel/render.rs +62 -11
  94. package/src/rust/components/main_panel/update.rs +39 -10
  95. package/src/rust/components/main_panel.rs +26 -2
  96. package/src/rust/components/mod.rs +5 -7
  97. package/src/rust/components/new_panel_menu.rs +97 -0
  98. package/src/rust/components/number_series_style.rs +2 -3
  99. package/src/rust/components/panel_menu.rs +61 -87
  100. package/src/rust/components/panel_tab.rs +100 -43
  101. package/src/rust/components/plugin_tab.rs +125 -87
  102. package/src/rust/components/render_warning.rs +2 -2
  103. package/src/rust/components/{status_bar_counter.rs → rows_counter.rs} +10 -10
  104. package/src/rust/components/settings_panel.rs +3 -2
  105. package/src/rust/components/status_bar.rs +113 -20
  106. package/src/rust/components/status_indicator.rs +5 -14
  107. package/src/rust/components/string_column_style.rs +1 -2
  108. package/src/rust/components/{style/mod.rs → style.rs} +32 -15
  109. package/src/rust/components/style_controls/number_string_format/digits_section.rs +25 -15
  110. package/src/rust/components/style_controls/number_string_format/misc_section.rs +7 -3
  111. package/src/rust/components/style_controls/number_string_format/style_section.rs +2 -1
  112. package/src/rust/components/style_controls/number_string_format/types.rs +24 -0
  113. package/src/rust/components/style_controls/number_string_format.rs +37 -14
  114. package/src/rust/components/viewer/msg.rs +43 -30
  115. package/src/rust/components/viewer/panels.rs +50 -19
  116. package/src/rust/components/viewer/render.rs +15 -13
  117. package/src/rust/components/viewer/settings.rs +137 -68
  118. package/src/rust/components/viewer/snapshots.rs +5 -10
  119. package/src/rust/components/viewer/wiring.rs +1 -21
  120. package/src/rust/components/viewer.rs +15 -10
  121. package/src/rust/components/window_editor.rs +8 -9
  122. package/src/rust/config/column_config_schema.rs +358 -52
  123. package/src/rust/config/datetime_column_style.rs +4 -21
  124. package/src/rust/config/mod.rs +0 -2
  125. package/src/rust/config/number_string_format.rs +211 -18
  126. package/src/rust/config/workspace_config.rs +2 -1
  127. package/src/rust/custom_elements/debug_plugin.rs +1 -10
  128. package/src/rust/custom_elements/mod.rs +0 -3
  129. package/src/rust/custom_elements/viewer.rs +0 -18
  130. package/src/rust/js/plugin.rs +0 -86
  131. package/src/rust/lib.rs +12 -15
  132. package/src/rust/presentation/column_locator.rs +3 -7
  133. package/src/rust/presentation.rs +85 -38
  134. package/src/rust/queries/hosted_tables.rs +38 -0
  135. package/src/rust/queries/mod.rs +2 -0
  136. package/src/rust/queries/palette_set.rs +1 -1
  137. package/src/rust/queries/plugin_column_styles.rs +2 -2
  138. package/src/rust/renderer/activate.rs +1 -3
  139. package/src/rust/renderer/dispatch.rs +117 -30
  140. package/src/rust/renderer/limits.rs +24 -5
  141. package/src/rust/renderer/plugin_config.rs +56 -8
  142. package/src/rust/renderer/props.rs +2 -1
  143. package/src/rust/renderer.rs +14 -27
  144. package/src/rust/session/props.rs +4 -3
  145. package/src/rust/session/view_subscription.rs +19 -6
  146. package/src/rust/session.rs +38 -70
  147. package/src/rust/tasks/create_panel.rs +1 -1
  148. package/src/rust/tasks/dismiss_render_warning.rs +1 -1
  149. package/src/rust/tasks/eject.rs +1 -0
  150. package/src/rust/tasks/pipeline.rs +0 -60
  151. package/src/rust/tasks/presize_panels.rs +24 -1
  152. package/src/rust/tasks/restore_and_render.rs +0 -17
  153. package/src/rust/tasks/send_column_config.rs +1 -1
  154. package/src/rust/tasks/send_plugin_config.rs +1 -1
  155. package/src/rust/tasks/set_edit_mode.rs +1 -1
  156. package/src/rust/ui/containers/control_group.rs +113 -0
  157. package/src/rust/{components → ui}/containers/dropdown_menu.rs +5 -2
  158. package/src/rust/{components → ui}/containers/mod.rs +1 -4
  159. package/src/rust/{components → ui}/containers/scroll_panel.rs +0 -22
  160. package/src/rust/{components → ui}/containers/scroll_panel_item.rs +0 -13
  161. package/src/rust/{components → ui}/containers/select.rs +8 -8
  162. package/src/rust/{components → ui}/containers/sidebar.rs +5 -32
  163. package/src/rust/{components → ui}/containers/split_panel.rs +8 -80
  164. package/src/rust/{components → ui}/containers/tab_list.rs +15 -9
  165. package/src/rust/{components → ui}/context_menu.rs +8 -13
  166. package/src/rust/{presentation/drag_helpers.rs → ui/dragdrop.rs} +26 -31
  167. package/src/rust/{components → ui}/font_loader.rs +0 -26
  168. package/src/rust/{components → ui}/form/color_selector.rs +4 -2
  169. package/src/rust/ui/form/intl_label.rs +81 -0
  170. package/src/rust/{components → ui}/form/mirrored_textarea.rs +0 -53
  171. package/src/rust/{components → ui}/form/mod.rs +1 -10
  172. package/src/rust/{components → ui}/form/multi_stop_gradient_selector.rs +4 -5
  173. package/src/rust/{components → ui}/form/named_value_picker.rs +1 -6
  174. package/src/rust/{components → ui}/form/number_field.rs +1 -1
  175. package/src/rust/{components → ui}/form/number_range_field.rs +1 -1
  176. package/src/rust/{components → ui}/form/optional_field.rs +3 -1
  177. package/src/rust/{components → ui}/form/palette_selector.rs +4 -5
  178. package/src/rust/{components → ui}/form/select_enum_field.rs +2 -2
  179. package/src/rust/{components → ui}/form/select_value_field.rs +2 -2
  180. package/src/rust/ui/mod.rs +56 -0
  181. package/src/rust/ui/modal.rs +61 -0
  182. package/src/rust/{components → ui}/portal.rs +174 -79
  183. package/src/rust/{components/style/style_provider.rs → ui/style.rs} +4 -34
  184. package/src/rust/utils/completion.rs +0 -6
  185. package/src/rust/{config/css_scale.rs → utils/css.rs} +38 -35
  186. package/src/rust/utils/mod.rs +2 -0
  187. package/src/rust/workspace.rs +35 -0
  188. package/src/svg/boolean-type.svg +3 -2
  189. package/src/svg/checkbox-checked-icon.svg +4 -7
  190. package/src/svg/checkbox-hover.svg +3 -3
  191. package/src/svg/checkbox-off.svg +2 -3
  192. package/src/svg/checkbox-on.svg +3 -3
  193. package/src/svg/checkbox-unchecked-icon.svg +3 -3
  194. package/src/svg/datagrid-select-column.svg +8 -15
  195. package/src/svg/datagrid-select-region.svg +8 -15
  196. package/src/svg/datagrid-select-row-tree.svg +7 -12
  197. package/src/svg/datagrid-select-row.svg +8 -13
  198. package/src/svg/date-type.svg +4 -23
  199. package/src/svg/drawer-tab-hover.svg +4 -6
  200. package/src/svg/drawer-tab-invert-hover.svg +5 -5
  201. package/src/svg/drawer-tab-invert.svg +3 -5
  202. package/src/svg/drawer-tab.svg +5 -5
  203. package/src/svg/mega-menu-icons-candlestick.svg +5 -37
  204. package/src/svg/mega-menu-icons-ohlc.svg +4 -39
  205. package/src/svg/mega-menu-icons-sunburst.svg +4 -38
  206. package/src/svg/mega-menu-icons-treemap.svg +3 -43
  207. package/src/svg/mega-menu-icons-y-scatter.svg +12 -72
  208. package/src/svg/new-panel-icon.svg +4 -0
  209. package/src/svg/number-type.svg +3 -3
  210. package/src/svg/radio-hover.svg +3 -3
  211. package/src/svg/radio-off.svg +3 -3
  212. package/src/svg/radio-on.svg +3 -3
  213. package/src/svg/status_ok.svg +3 -26
  214. package/src/svg/string-type.svg +4 -8
  215. package/src/svg/theme-icon.svg +5 -5
  216. package/src/themes/blueprint.css +133 -0
  217. package/src/themes/botanical.css +3 -52
  218. package/src/themes/defaults.css +3 -144
  219. package/src/themes/dracula.css +1 -71
  220. package/src/themes/eggplant.css +131 -0
  221. package/src/themes/gruvbox-dark.css +2 -79
  222. package/src/themes/gruvbox.css +2 -40
  223. package/src/themes/icons.css +2 -10
  224. package/src/themes/intl/ar.css +185 -0
  225. package/src/themes/intl/de.css +65 -33
  226. package/src/themes/intl/es.css +60 -28
  227. package/src/themes/intl/fr.css +60 -28
  228. package/src/themes/intl/ja.css +62 -30
  229. package/src/themes/intl/pt.css +58 -26
  230. package/src/themes/intl/zh.css +68 -36
  231. package/src/themes/intl.css +42 -7
  232. package/src/themes/ledger.css +137 -0
  233. package/src/themes/monokai.css +1 -64
  234. package/src/themes/nord.css +131 -0
  235. package/src/themes/phosphor.css +1 -61
  236. package/src/themes/pro-dark.css +1 -52
  237. package/src/themes/pro.css +98 -0
  238. package/src/themes/solarized-dark.css +1 -46
  239. package/src/themes/solarized.css +1 -24
  240. package/src/themes/themes.css +10 -5
  241. package/src/themes/vaporwave.css +1 -57
  242. package/src/themes/velvet.css +132 -0
  243. package/src/ts/column-format.ts +52 -2
  244. package/src/ts/extensions.ts +0 -13
  245. package/src/ts/ts-rs/ViewOnUpdateResp.ts +1 -1
  246. package/src/css/plugin-settings-panel.css +0 -99
  247. package/src/rust/components/modal.rs +0 -177
  248. package/src/rust/custom_elements/copy_dropdown.rs +0 -219
  249. package/src/rust/custom_elements/export_dropdown.rs +0 -216
  250. package/src/rust/custom_elements/modal.rs +0 -299
  251. package/src/svg/dropdown-selector-light.svg +0 -3
  252. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline0.js +0 -0
  253. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline1.js +0 -0
  254. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline2.js +0 -0
  255. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline3.js +0 -0
  256. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline4.js +0 -0
  257. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline5.js +0 -0
  258. /package/src/css/{containers → ui/containers}/scroll-panel.css +0 -0
  259. /package/src/css/{containers → ui/containers}/split-panel.css +0 -0
  260. /package/src/css/{dom → ui/dom}/checkbox.css +0 -0
  261. /package/src/css/{dom → ui/dom}/scrollbar.css +0 -0
  262. /package/src/css/{form → ui/form}/mirrored-textarea.css +0 -0
  263. /package/src/rust/components/{form → code_editor}/highlight.rs +0 -0
  264. /package/src/rust/{components → ui}/containers/sidebar_close_button.rs +0 -0
  265. /package/src/rust/{components → ui}/form/number_input.rs +0 -0
@@ -19,31 +19,61 @@ use wasm_bindgen::prelude::*;
19
19
  use web_sys::*;
20
20
  use yew::prelude::*;
21
21
 
22
- use crate::components::modal::ModalOrientation;
23
- use crate::components::style::{StyleProvider, StyleSurface};
22
+ use crate::ui::modal::ModalOrientation;
23
+ use crate::ui::style::StyleProvider;
24
24
  use crate::utils::*;
25
25
 
26
+ /// The `slot` every modal host is assigned in the viewer's light DOM, so it
27
+ /// is in the flat tree.
28
+ pub const MODAL_SLOT: &str = "modal";
29
+
30
+ /// Find the `<perspective-viewer>` that `target` lives in — as a light-DOM
31
+ /// descendant, or through any number of shadow roots.
32
+ fn nearest_viewer(target: &HtmlElement) -> Option<HtmlElement> {
33
+ let is_viewer = |el: &Element| el.tag_name().eq_ignore_ascii_case("perspective-viewer");
34
+ if let Ok(Some(viewer)) = target.closest("perspective-viewer") {
35
+ return Some(viewer.unchecked_into());
36
+ }
37
+
38
+ let mut node: Node = target.clone().into();
39
+ loop {
40
+ let root = node.get_root_node();
41
+ let host = root.dyn_ref::<ShadowRoot>()?.host();
42
+ if is_viewer(&host) {
43
+ return Some(host.unchecked_into());
44
+ }
45
+
46
+ if let Ok(Some(viewer)) = host.closest("perspective-viewer") {
47
+ return Some(viewer.unchecked_into());
48
+ }
49
+
50
+ node = host.into();
51
+ }
52
+ }
53
+
54
+ /// A modal (menu / completion dropdown) anchored to a `target` element,
55
+ /// hosted as a light-DOM child of the `<perspective-viewer>` in the top layer.
26
56
  #[derive(Properties, PartialEq)]
27
57
  pub struct PortalModalProps {
28
58
  pub children: Children,
29
-
30
- /// The element to position relative to. `None` means closed.
31
59
  pub target: Option<HtmlElement>,
60
+ pub tag_name: &'static str,
61
+ pub theme: String,
62
+ pub sheet: web_sys::CssStyleSheet,
32
63
 
33
- /// Whether the portal manages its own focus and closes on blur.
64
+ /// Adds this component's own light-dismiss (an outside `pointerdown` or
65
+ /// Escape → `on_close`) instead of `popover="auto"`, whose platform
66
+ /// dismissal fires on the opening click's own pointerup.
34
67
  #[prop_or(true)]
35
68
  pub own_focus: bool,
36
69
 
37
- /// Called when the portal closes (blur, etc).
38
70
  #[prop_or_default]
39
71
  pub on_close: Callback<()>,
40
72
 
41
- pub tag_name: &'static str,
42
-
43
- pub theme: String,
44
-
45
- /// Which popup CSS surface this portal's ShadowRoot adopts.
46
- pub surface: StyleSurface,
73
+ /// The `<perspective-viewer>` to mount under, defaulting to the nearest
74
+ /// viewer around `target`.
75
+ #[prop_or_default]
76
+ pub mount: Option<HtmlElement>,
47
77
  }
48
78
 
49
79
  pub enum PortalModalMsg {
@@ -58,28 +88,49 @@ pub struct PortalModal {
58
88
  visible: bool,
59
89
  rev_vert: ModalOrientation,
60
90
  anchor: Rc<Cell<ModalAnchor>>,
61
- _blur_closure: Option<Closure<dyn FnMut(FocusEvent)>>,
91
+ /// The `own_focus` light-dismiss listeners (document-level, capture),
92
+ /// present only while the modal is open.
93
+ _dismiss: Option<DismissHandlers>,
94
+ }
95
+
96
+ /// The dismiss closures of an open `own_focus` modal: outside `pointerdown`
97
+ /// and Escape `keydown` on the document (capture), `blur` on the host.
98
+ struct DismissHandlers {
99
+ pointerdown: Closure<dyn FnMut(PointerEvent)>,
100
+ keydown: Closure<dyn FnMut(KeyboardEvent)>,
101
+ blur: Closure<dyn FnMut(FocusEvent)>,
62
102
  }
63
103
 
64
104
  impl PortalModal {
65
- fn attach_to_body(&self) {
66
- if !self.host.is_connected() {
67
- let _ = global::body().append_child(&self.host);
105
+ fn attach(&self, ctx: &Context<Self>, target: &HtmlElement) {
106
+ if self.host.is_connected() {
107
+ return;
68
108
  }
109
+
110
+ let parent = ctx
111
+ .props()
112
+ .mount
113
+ .clone()
114
+ .or_else(|| nearest_viewer(target))
115
+ .unwrap_or_else(|| {
116
+ tracing::warn!(
117
+ "`{}` target is outside any <perspective-viewer>; mounting on <body> unthemed",
118
+ ctx.props().tag_name
119
+ );
120
+
121
+ global::body()
122
+ });
123
+
124
+ let _ = parent.append_child(&self.host);
125
+ let _ = self.host.show_popover();
69
126
  }
70
127
 
71
- fn detach_from_body(&mut self) {
128
+ fn detach(&mut self) {
129
+ self.remove_dismiss_handlers();
72
130
  if self.host.is_connected() {
73
- let _ = global::body().remove_child(&self.host);
74
- }
75
-
76
- if let Some(closure) = self._blur_closure.as_ref() {
77
- self.host
78
- .remove_event_listener_with_callback("blur", closure.as_ref().unchecked_ref())
79
- .unwrap()
131
+ let _ = self.host.hide_popover();
132
+ self.host.remove();
80
133
  }
81
-
82
- self._blur_closure = None;
83
134
  }
84
135
 
85
136
  fn position_against_target(&mut self, target: &HtmlElement) {
@@ -90,12 +141,10 @@ impl PortalModal {
90
141
  let left = target_rect.left();
91
142
 
92
143
  if !self.visible {
93
- // First pass: position at default anchor, invisible
94
144
  self.top = top + height - 1.0;
95
145
  self.left = left;
96
146
  self.visible = false;
97
147
  } else {
98
- // Second pass: compute actual anchor and reposition
99
148
  let anchor = calc_relative_position(&self.host, top, left, height, width);
100
149
  self.anchor.set(anchor);
101
150
  let modal_rect = self.host.get_bounding_client_rect();
@@ -104,9 +153,15 @@ impl PortalModal {
104
153
  self.left = new_left;
105
154
  self.rev_vert.set(anchor.is_rev_vert());
106
155
  }
156
+
157
+ let style = self.host.style();
158
+ let _ = style.set_property("top", &format!("{}px", self.top));
159
+ let _ = style.set_property("left", &format!("{}px", self.left));
107
160
  }
108
161
 
109
- fn setup_blur_handler(&mut self, ctx: &Context<Self>) {
162
+ /// This component's light-dismiss for `own_focus` modals: an outside
163
+ /// `pointerdown` (document capture) or Escape.
164
+ fn setup_dismiss_handlers(&mut self, ctx: &Context<Self>) {
110
165
  let on_close = {
111
166
  let target = ctx.props().target.clone();
112
167
  ctx.props().on_close.reform(move |_| {
@@ -116,15 +171,77 @@ impl PortalModal {
116
171
  })
117
172
  };
118
173
 
119
- let closure = Closure::wrap(Box::new(move |_: FocusEvent| {
174
+ let host = self.host.clone();
175
+ let pointerdown = Closure::wrap(Box::new({
176
+ let on_close = on_close.clone();
177
+ move |event: PointerEvent| {
178
+ if !event.composed_path().includes(host.as_ref(), 0) {
179
+ on_close.emit(());
180
+ }
181
+ }
182
+ }) as Box<dyn FnMut(PointerEvent)>);
183
+
184
+ let keydown = Closure::wrap(Box::new({
185
+ let on_close = on_close.clone();
186
+ move |event: KeyboardEvent| {
187
+ if event.key() == "Escape" {
188
+ on_close.emit(());
189
+ }
190
+ }
191
+ }) as Box<dyn FnMut(KeyboardEvent)>);
192
+
193
+ let blur = Closure::wrap(Box::new(move |_: FocusEvent| {
120
194
  on_close.emit(());
121
195
  }) as Box<dyn FnMut(FocusEvent)>);
122
196
 
123
197
  let _ = self
124
198
  .host
125
- .add_event_listener_with_callback("blur", closure.as_ref().unchecked_ref());
199
+ .add_event_listener_with_callback("blur", blur.as_ref().unchecked_ref());
126
200
 
127
- self._blur_closure = Some(closure);
201
+ let document = global::document();
202
+ let _ = document.add_event_listener_with_callback_and_bool(
203
+ "pointerdown",
204
+ pointerdown.as_ref().unchecked_ref(),
205
+ true,
206
+ );
207
+
208
+ let _ = document.add_event_listener_with_callback_and_bool(
209
+ "keydown",
210
+ keydown.as_ref().unchecked_ref(),
211
+ true,
212
+ );
213
+
214
+ self._dismiss = Some(DismissHandlers {
215
+ pointerdown,
216
+ keydown,
217
+ blur,
218
+ });
219
+ }
220
+
221
+ fn remove_dismiss_handlers(&mut self) {
222
+ if let Some(DismissHandlers {
223
+ pointerdown,
224
+ keydown,
225
+ blur,
226
+ }) = self._dismiss.take()
227
+ {
228
+ let _ = self
229
+ .host
230
+ .remove_event_listener_with_callback("blur", blur.as_ref().unchecked_ref());
231
+
232
+ let document = global::document();
233
+ let _ = document.remove_event_listener_with_callback_and_bool(
234
+ "pointerdown",
235
+ pointerdown.as_ref().unchecked_ref(),
236
+ true,
237
+ );
238
+
239
+ let _ = document.remove_event_listener_with_callback_and_bool(
240
+ "keydown",
241
+ keydown.as_ref().unchecked_ref(),
242
+ true,
243
+ );
244
+ }
128
245
  }
129
246
  }
130
247
 
@@ -138,8 +255,14 @@ impl Component for PortalModal {
138
255
  .unwrap()
139
256
  .unchecked_into();
140
257
 
141
- host.style().set_property("position", "fixed").unwrap();
142
- host.style().set_property("z-index", "10000").unwrap();
258
+ host.set_attribute("popover", "manual").unwrap();
259
+ host.set_attribute("slot", MODAL_SLOT).unwrap();
260
+
261
+ let style = host.style();
262
+ style.set_property("position", "fixed").unwrap();
263
+ style.set_property("right", "auto").unwrap();
264
+ style.set_property("bottom", "auto").unwrap();
265
+ style.set_property("margin", "0").unwrap();
143
266
  let init = ShadowRootInit::new(ShadowRootMode::Open);
144
267
  let shadow_root = if let Some(elem) = host.shadow_root() {
145
268
  elem
@@ -156,7 +279,7 @@ impl Component for PortalModal {
156
279
  visible: false,
157
280
  rev_vert: Default::default(),
158
281
  anchor: Default::default(),
159
- _blur_closure: None,
282
+ _dismiss: None,
160
283
  }
161
284
  }
162
285
 
@@ -170,32 +293,20 @@ impl Component for PortalModal {
170
293
  }
171
294
 
172
295
  fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
173
- // The host element (`tag_name`) and its adopted sheet (`surface`) are
174
- // fixed at `create` — a swap at the same vdom position must be keyed
175
- // so Yew recreates the component (see `PanelMenu`'s menu→picker
176
- // stages).
177
296
  debug_assert_eq!(ctx.props().tag_name, old_props.tag_name);
178
- debug_assert_eq!(ctx.props().surface, old_props.surface);
179
-
180
- let new_target = &ctx.props().target;
181
- let old_target = &old_props.target;
297
+ debug_assert_eq!(ctx.props().sheet, old_props.sheet);
182
298
 
183
- match (old_target, new_target, self._blur_closure.as_ref()) {
184
- (None, Some(_), Some(closure)) => {
299
+ match (&old_props.target, &ctx.props().target) {
300
+ (None, Some(_)) => {
185
301
  self.visible = false;
186
- self.host
187
- .remove_event_listener_with_callback("blur", closure.as_ref().unchecked_ref())
188
- .unwrap();
189
-
190
- self._blur_closure = None;
302
+ self.remove_dismiss_handlers();
191
303
  },
192
- (None, Some(_), None) => {
193
- self.visible = false;
194
- self._blur_closure = None;
304
+ (Some(old), None) => {
305
+ let _ = old.class_list().remove_1("modal-target");
306
+ self.detach();
195
307
  },
196
- (Some(_), None, _) => {
197
- self.detach_from_body();
198
- return true;
308
+ (Some(old), Some(new)) if old != new => {
309
+ let _ = old.class_list().remove_1("modal-target");
199
310
  },
200
311
  _ => {},
201
312
  }
@@ -209,17 +320,13 @@ impl Component for PortalModal {
209
320
  return html! {};
210
321
  }
211
322
 
212
- let opacity = if self.visible { "" } else { ";opacity:0" };
213
- let css = format!(
214
- ":host{{top:{}px;left:{}px{}}}",
215
- self.top, self.left, opacity
216
- );
323
+ let css = if self.visible { "" } else { ":host{opacity:0}" };
217
324
 
218
325
  let portal_content = html! {
219
326
  <>
220
327
  <style>{ css }</style>
221
328
  <ContextProvider<ModalOrientation> context={self.rev_vert.clone()}>
222
- <StyleProvider root={self.host.clone()} surface={ctx.props().surface}>
329
+ <StyleProvider root={self.host.clone()} sheet={ctx.props().sheet.clone()}>
223
330
  { for ctx.props().children.iter() }
224
331
  </StyleProvider>
225
332
  </ContextProvider<ModalOrientation>>
@@ -234,34 +341,26 @@ impl Component for PortalModal {
234
341
  if !self.host.is_connected() {
235
342
  let theme = ctx.props().theme.as_str();
236
343
  self.host.set_attribute("theme", theme).unwrap();
237
-
238
- // First render with a target: attach to body, position invisible
239
- self.position_against_target(target);
240
- self.attach_to_body();
241
-
242
- // Propagate theme from target
243
344
  if let Some(theme) = target.get_attribute("theme") {
244
345
  let _ = self.host.set_attribute("theme", &theme);
245
346
  }
246
347
 
348
+ self.position_against_target(target);
349
+ self.attach(ctx, target);
247
350
  target.class_list().add_1("modal-target").unwrap();
248
-
249
351
  if ctx.props().own_focus {
250
352
  self.host.set_attribute("tabindex", "0").unwrap();
251
- self.setup_blur_handler(ctx);
353
+ self.setup_dismiss_handlers(ctx);
252
354
  }
253
355
 
254
- // Schedule second positioning pass
255
356
  let link = ctx.link().clone();
256
357
  wasm_bindgen_futures::spawn_local(async move {
257
358
  request_animation_frame().await;
258
359
  link.send_message(PortalModalMsg::Reposition);
259
360
  });
260
361
  } else if self.visible {
261
- // Second pass: reposition with correct anchor
262
362
  self.position_against_target(target);
263
-
264
- if ctx.props().own_focus && self._blur_closure.is_some() {
363
+ if ctx.props().own_focus && self._dismiss.is_some() {
265
364
  let _ = self.host.focus();
266
365
  }
267
366
  }
@@ -271,14 +370,10 @@ impl Component for PortalModal {
271
370
  fn destroy(&mut self, ctx: &Context<Self>) {
272
371
  if let Some(target) = &ctx.props().target {
273
372
  target.class_list().remove_1("modal-target").unwrap();
274
- if target.get_attribute("theme").is_some() {
275
- let _ = self.host.remove_attribute("theme");
276
- }
277
-
278
373
  let event = CustomEvent::new("-perspective-close-expression").unwrap();
279
374
  let _ = target.dispatch_event(&event);
280
375
  }
281
376
 
282
- self.detach_from_body();
377
+ self.detach();
283
378
  }
284
379
  }
@@ -13,42 +13,15 @@
13
13
  use wasm_bindgen::{JsCast, JsValue};
14
14
  use yew::prelude::*;
15
15
 
16
- use super::surface_sheet;
17
-
18
- /// Which prebuilt CSS bundle a [`StyleProvider`] adopts into its root.
19
- #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
20
- pub enum StyleSurface {
21
- /// The `<perspective-viewer>` ShadowRoot.
22
- #[default]
23
- Viewer,
24
- /// Column-name completion dropdown (inverted color scheme).
25
- ColumnDropdown,
26
- /// Filter-value completion dropdown.
27
- FilterDropdown,
28
- /// Expression function-completion dropdown.
29
- FunctionDropdown,
30
- /// Copy / Export menu (non-inverted color scheme).
31
- DropdownMenu,
32
- /// Panel context menu (right-click a panel).
33
- ContextMenu,
34
- }
35
-
36
16
  #[derive(Properties, PartialEq)]
37
17
  pub struct StyleProviderProps {
38
18
  pub root: web_sys::HtmlElement,
39
-
40
19
  #[prop_or(true)]
41
20
  pub is_shadow: bool,
42
-
43
- #[prop_or_default]
44
- pub surface: StyleSurface,
45
-
21
+ pub sheet: web_sys::CssStyleSheet,
46
22
  pub children: Children,
47
23
  }
48
24
 
49
- /// Adopts the build-time CSS bundle for its [`StyleSurface`] into the root's
50
- /// `adoptedStyleSheets` once, on creation. Unlike the previous per-snippet
51
- /// `LocalStyle` registration, children no longer register CSS individually.
52
25
  pub struct StyleProvider;
53
26
 
54
27
  impl Component for StyleProvider {
@@ -56,7 +29,7 @@ impl Component for StyleProvider {
56
29
  type Properties = StyleProviderProps;
57
30
 
58
31
  fn create(ctx: &Context<Self>) -> Self {
59
- adopt_surface_sheet(ctx.props());
32
+ adopt_sheet(ctx.props());
60
33
  Self
61
34
  }
62
35
 
@@ -65,9 +38,7 @@ impl Component for StyleProvider {
65
38
  }
66
39
  }
67
40
 
68
- /// Push the surface's shared stylesheet onto the root's `adoptedStyleSheets`,
69
- /// unless it is already present (a root may host more than one provider).
70
- fn adopt_surface_sheet(props: &StyleProviderProps) {
41
+ fn adopt_sheet(props: &StyleProviderProps) {
71
42
  let root: JsValue = if props.is_shadow {
72
43
  props.root.shadow_root().unwrap().into()
73
44
  } else {
@@ -78,8 +49,7 @@ fn adopt_surface_sheet(props: &StyleProviderProps) {
78
49
  .unwrap()
79
50
  .unchecked_into::<js_sys::Array>();
80
51
 
81
- let sheet = surface_sheet(props.surface);
82
- let sheet_val: &JsValue = sheet.as_ref();
52
+ let sheet_val: &JsValue = props.sheet.as_ref();
83
53
  if sheets.index_of(sheet_val, 0) < 0 {
84
54
  sheets.push(sheet_val);
85
55
  }
@@ -15,12 +15,6 @@ use std::future::Future;
15
15
  use futures::channel::oneshot;
16
16
  use perspective_js::utils::*;
17
17
 
18
- /// A completion handle for message-based public API methods (invariant I6 —
19
- /// see `SESSION_CONFIG_COHERENCE_PLAN.md`): its ONLY resolution API is
20
- /// [`Completion::resolve_after`], which takes a run future — so resolving a
21
- /// public method's promise at message-handling time (before the renders it
22
- /// caused have drawn) is unwritable, not merely discouraged. Dropping an
23
- /// unresolved `Completion` rejects the caller as cancelled.
24
18
  pub struct Completion(Option<oneshot::Sender<ApiResult<()>>>);
25
19
 
26
20
  impl Completion {
@@ -12,9 +12,28 @@
12
12
 
13
13
  use std::collections::BTreeMap;
14
14
 
15
+ use serde::{Deserialize, Serialize};
15
16
  use serde_json::Value;
16
17
 
17
- use super::{GradientStopSpec, canonicalize_gradient_stops};
18
+ #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
19
+ pub struct GradientStopSpec {
20
+ pub color: String,
21
+ pub offset: f64,
22
+ }
23
+
24
+ pub fn canonicalize_gradient_stops(mut stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
25
+ for stop in &mut stops {
26
+ stop.offset = (stop.offset.clamp(0.0, 1.0) * 1000.0).round() / 1000.0;
27
+ }
28
+
29
+ stops.sort_by(|a, b| {
30
+ a.offset
31
+ .partial_cmp(&b.offset)
32
+ .unwrap_or(std::cmp::Ordering::Equal)
33
+ });
34
+
35
+ stops
36
+ }
18
37
 
19
38
  pub const USER_VAR_PREFIX: &str = "--psp-user--";
20
39
 
@@ -64,8 +83,8 @@ impl CssKind {
64
83
  }
65
84
  }
66
85
 
67
- /// The property name a whole-value `var(--psp-user--…)` reference
68
- /// names, or `None` when `src` is not one.
86
+ /// The property name a whole-value `var(--psp-user--…)` reference names, or
87
+ /// `None` when `src` is not one.
69
88
  pub fn parse_var_ref(src: &str) -> Option<&str> {
70
89
  let inner = src.trim().strip_prefix("var(")?.strip_suffix(')')?.trim();
71
90
  if !inner.starts_with(USER_VAR_PREFIX)
@@ -129,8 +148,8 @@ fn channel_from_token(token: &str) -> Result<u8, String> {
129
148
  Ok(value.round().clamp(0.0, 255.0) as u8)
130
149
  }
131
150
 
132
- /// A color literal — `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()`
133
- /// or `rgba()` — normalized to lowercase `#rrggbb`.
151
+ /// A color literal — `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()` or
152
+ /// `rgba()` — normalized to lowercase `#rrggbb`.
134
153
  pub fn normalize_css_color(src: &str) -> Result<String, String> {
135
154
  let src = src.trim();
136
155
  if let Some(hex) = src.strip_prefix('#') {
@@ -260,8 +279,8 @@ fn format_percent(offset: f64) -> String {
260
279
  format!("{text}%")
261
280
  }
262
281
 
263
- /// The canonical gradient string for `stops`: `linear-gradient(to
264
- /// right, #rrggbb P%, …)` with every position explicit.
282
+ /// The canonical gradient string for `stops`: `linear-gradient(to right,
283
+ /// #rrggbb P%, …)` with every position explicit.
265
284
  pub fn gradient_to_css(stops: &[GradientStopSpec]) -> String {
266
285
  let body = canonicalize_gradient_stops(stops.to_vec())
267
286
  .iter()
@@ -272,26 +291,20 @@ pub fn gradient_to_css(stops: &[GradientStopSpec]) -> String {
272
291
  format!("linear-gradient(to right, {body})")
273
292
  }
274
293
 
275
- /// The canonical palette string for `colors`: `linear-gradient(to
276
- /// right, #rrggbb, …)` with no positions.
294
+ /// The canonical palette string for `colors`: `linear-gradient(to right,
295
+ /// #rrggbb, …)` with no positions.
277
296
  pub fn palette_to_css(colors: &[String]) -> String {
278
297
  format!("linear-gradient(to right, {})", colors.join(", "))
279
298
  }
280
299
 
281
- /// A serialized [`ControlSpec::GradientStops`] value.
282
- ///
283
- /// [`ControlSpec::GradientStops`]: super::ControlSpec::GradientStops
300
+ /// A serialized `ControlSpec::GradientStops` value.
284
301
  #[derive(Clone, Debug, PartialEq)]
285
302
  pub enum CssGradient {
286
- /// Canonical stops (sorted, rounded), length ≥ 2.
287
303
  Literal(Vec<GradientStopSpec>),
288
- /// A `--psp-user--gradient-…` property name.
289
304
  Var(String),
290
305
  }
291
306
 
292
307
  impl CssGradient {
293
- /// Parse a gradient string of at least 2 stops, with optional
294
- /// positions filled by the CSS implicit rules.
295
308
  pub fn parse(src: &str) -> Result<Self, String> {
296
309
  if let Some(name) = parse_kind_var(src, CssKind::Gradient)? {
297
310
  return Ok(CssGradient::Var(name));
@@ -348,20 +361,14 @@ impl CssGradient {
348
361
  }
349
362
  }
350
363
 
351
- /// A serialized [`ControlSpec::Palette`] value.
352
- ///
353
- /// [`ControlSpec::Palette`]: super::ControlSpec::Palette
364
+ /// A serialized `ControlSpec::Palette` value.
354
365
  #[derive(Clone, Debug, PartialEq)]
355
366
  pub enum CssPalette {
356
- /// Ordered `#rrggbb` entries, length ≥ 1.
357
367
  Literal(Vec<String>),
358
- /// A `--psp-user--palette-…` property name.
359
368
  Var(String),
360
369
  }
361
370
 
362
371
  impl CssPalette {
363
- /// Parse a palette string of at least 1 entry, with positions
364
- /// forbidden.
365
372
  pub fn parse(src: &str) -> Result<Self, String> {
366
373
  if let Some(name) = parse_kind_var(src, CssKind::Palette)? {
367
374
  return Ok(CssPalette::Var(name));
@@ -395,14 +402,10 @@ impl CssPalette {
395
402
  }
396
403
  }
397
404
 
398
- /// A serialized [`ControlSpec::Color`] value.
399
- ///
400
- /// [`ControlSpec::Color`]: super::ControlSpec::Color
405
+ /// A serialized `ControlSpec::Color` value.
401
406
  #[derive(Clone, Debug, PartialEq)]
402
407
  pub enum CssColor {
403
- /// Lowercase `#rrggbb`.
404
408
  Literal(String),
405
- /// A `--psp-user--color-…` property name.
406
409
  Var(String),
407
410
  }
408
411
 
@@ -435,8 +438,8 @@ pub fn canonicalize_css_color(src: &str) -> Result<String, String> {
435
438
  CssKind::Color.canonicalize(src)
436
439
  }
437
440
 
438
- /// One CSS literal stored in a panel's `columns_config`: its column,
439
- /// key, schema kind and canonical value — the unit of "in use".
441
+ /// One CSS literal stored in a panel's `columns_config`: its column, key,
442
+ /// schema kind and canonical value — the unit of "in use".
440
443
  #[derive(Clone, Debug, PartialEq)]
441
444
  pub struct CssLiteralUse {
442
445
  pub column: String,
@@ -445,8 +448,8 @@ pub struct CssLiteralUse {
445
448
  pub literal: String,
446
449
  }
447
450
 
448
- /// The workspace palette: `restored` unioned with every in-use
449
- /// literal, each named deterministically.
451
+ /// The workspace palette: `restored` unioned with every in-use literal, each
452
+ /// named deterministically.
450
453
  pub fn assign_palette_names(
451
454
  restored: &BTreeMap<String, String>,
452
455
  host: &[NamedValue],
@@ -487,9 +490,9 @@ pub fn palette_name_for(
487
490
  .map(|(name, _)| name.clone())
488
491
  }
489
492
 
490
- /// Replace every whole-value `var(--psp-user--…)` string in one
491
- /// column's config entry with the literal `lookup` resolves it to,
492
- /// removing and reporting keys that do not resolve.
493
+ /// Replace every whole-value `var(--psp-user--…)` string in one column's config
494
+ /// entry with the literal `lookup` resolves it to, removing and reporting keys
495
+ /// that do not resolve.
493
496
  pub fn resolve_css_refs(
494
497
  entry: &mut serde_json::Map<String, Value>,
495
498
  lookup: &dyn Fn(&str) -> Option<String>,
@@ -18,6 +18,7 @@
18
18
 
19
19
  mod browser;
20
20
  mod completion;
21
+ mod css;
21
22
  mod css_vars;
22
23
  mod custom_element;
23
24
  mod datetime;
@@ -36,6 +37,7 @@ mod tests;
36
37
 
37
38
  pub use browser::*;
38
39
  pub use completion::*;
40
+ pub use css::*;
39
41
  pub use css_vars::*;
40
42
  pub use custom_element::*;
41
43
  pub use datetime::*;