@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
@@ -22,11 +22,10 @@ use perspective_js::utils::global::navigator;
22
22
  use wasm_bindgen::prelude::*;
23
23
  use yew::prelude::*;
24
24
 
25
- use super::modal::{ModalLink, SetModalLink};
26
25
  use crate::components::datetime_column_style::custom::DatetimeStyleCustom;
27
26
  use crate::components::datetime_column_style::simple::DatetimeStyleSimple;
28
- use crate::components::form::select_value_field::SelectValueField;
29
27
  use crate::config::*;
28
+ use crate::ui::{ModalLink, SelectValueField, SetModalLink};
30
29
  use crate::utils::WeakScope;
31
30
 
32
31
  /// Format-only widget for `datetime` columns. Renders the `date_format`
@@ -38,6 +37,9 @@ pub struct DatetimeColumnStyleProps {
38
37
  pub enable_time_config: bool,
39
38
  pub config: Option<DatetimeColumnStyleConfig>,
40
39
 
40
+ /// The active plugin's declared default `date_format`.
41
+ pub default_format: Rc<DatetimeFormatType>,
42
+
41
43
  #[prop_or_default]
42
44
  pub on_change: Callback<ColumnConfigFieldUpdate>,
43
45
 
@@ -57,7 +59,9 @@ impl ModalLink<DatetimeColumnStyle> for DatetimeColumnStyleProps {
57
59
 
58
60
  impl PartialEq for DatetimeColumnStyleProps {
59
61
  fn eq(&self, other: &Self) -> bool {
60
- self.enable_time_config == other.enable_time_config && self.config == other.config
62
+ self.enable_time_config == other.enable_time_config
63
+ && self.config == other.config
64
+ && self.default_format == other.default_format
61
65
  }
62
66
  }
63
67
 
@@ -90,7 +94,9 @@ impl Component for DatetimeColumnStyle {
90
94
  std::mem::swap(&mut self.config, &mut new_config);
91
95
  rerender = true;
92
96
  }
93
- if old.enable_time_config != ctx.props().enable_time_config {
97
+ if old.enable_time_config != ctx.props().enable_time_config
98
+ || old.default_format != ctx.props().default_format
99
+ {
94
100
  rerender = true;
95
101
  }
96
102
  rerender
@@ -99,22 +105,24 @@ impl Component for DatetimeColumnStyle {
99
105
  fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
100
106
  match msg {
101
107
  DatetimeColumnStyleMsg::TimezoneChanged(val) => {
108
+ let mut format = self.effective_format(ctx);
102
109
  if Some(&*USER_TIMEZONE) != val.as_ref() {
103
- *self.config.date_format.time_zone_mut() = val;
110
+ *format.time_zone_mut() = val;
104
111
  } else {
105
- *self.config.date_format.time_zone_mut() = None;
112
+ *format.time_zone_mut() = None;
106
113
  }
107
114
 
115
+ self.set_format(ctx, format);
108
116
  self.dispatch_config(ctx);
109
117
  true
110
118
  },
111
119
  DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged(simple) => {
112
- self.config.date_format = DatetimeFormatType::Simple(simple);
120
+ self.set_format(ctx, DatetimeFormatType::Simple(simple));
113
121
  self.dispatch_config(ctx);
114
122
  true
115
123
  },
116
124
  DatetimeColumnStyleMsg::CustomDatetimeStyleConfigChanged(custom) => {
117
- self.config.date_format = DatetimeFormatType::Custom(custom);
125
+ self.set_format(ctx, DatetimeFormatType::Custom(custom));
118
126
  self.dispatch_config(ctx);
119
127
  true
120
128
  },
@@ -122,6 +130,24 @@ impl Component for DatetimeColumnStyle {
122
130
  }
123
131
 
124
132
  fn view(&self, ctx: &Context<Self>) -> Html {
133
+ let format = self.effective_format(ctx);
134
+ let (date_style_default, time_style_default) = match &*ctx.props().default_format {
135
+ DatetimeFormatType::Simple(simple) => (simple.date_style, simple.time_style),
136
+ DatetimeFormatType::Custom(_) => {
137
+ (SimpleDatetimeFormat::Short, SimpleDatetimeFormat::Medium)
138
+ },
139
+ };
140
+
141
+ let simple_reset = {
142
+ let default_format = ctx.props().default_format.clone();
143
+ ctx.link().callback(move |_| {
144
+ DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged(match &*default_format {
145
+ DatetimeFormatType::Simple(simple) => simple.clone(),
146
+ DatetimeFormatType::Custom(_) => SimpleDatetimeStyleConfig::default(),
147
+ })
148
+ })
149
+ };
150
+
125
151
  html! {
126
152
  <>
127
153
  <div id="column-style-container" class="datetime-column-style-container">
@@ -131,10 +157,10 @@ impl Component for DatetimeColumnStyle {
131
157
  values={ALL_TIMEZONES.with(|x| (*x).clone())}
132
158
  default_value={(*USER_TIMEZONE).clone()}
133
159
  on_change={ctx.link().callback(DatetimeColumnStyleMsg::TimezoneChanged)}
134
- current_value={self.config.date_format.time_zone().as_ref().unwrap_or(&*USER_TIMEZONE).clone()}
160
+ current_value={format.time_zone().as_ref().unwrap_or(&*USER_TIMEZONE).clone()}
135
161
  />
136
162
  }
137
- if let DatetimeFormatType::Simple(config) = &self.config.date_format {
163
+ if let DatetimeFormatType::Simple(config) = &format {
138
164
  if ctx.props().enable_time_config {
139
165
  <div class="row">
140
166
  <button
@@ -149,15 +175,17 @@ impl Component for DatetimeColumnStyle {
149
175
  enable_time_config={ctx.props().enable_time_config}
150
176
  on_change={ctx.link().callback(DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged)}
151
177
  config={config.clone()}
178
+ {date_style_default}
179
+ {time_style_default}
152
180
  />
153
- } else if let DatetimeFormatType::Custom(config) = &self.config.date_format {
181
+ } else if let DatetimeFormatType::Custom(config) = &format {
154
182
  if ctx.props().enable_time_config {
155
183
  <div class="row">
156
184
  <button
157
185
  id="datetime_format"
158
186
  data-title="Custom"
159
187
  data-title-hover="Switch to Simple"
160
- onclick={ctx.link().callback(|_| DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged(SimpleDatetimeStyleConfig::default()))}
188
+ onclick={simple_reset}
161
189
  />
162
190
  </div>
163
191
  }
@@ -201,6 +229,17 @@ static USER_TIMEZONE: LazyLock<String> = LazyLock::new(|| {
201
229
  });
202
230
 
203
231
  impl DatetimeColumnStyle {
232
+ fn effective_format(&self, ctx: &Context<Self>) -> DatetimeFormatType {
233
+ self.config
234
+ .date_format
235
+ .clone()
236
+ .unwrap_or_else(|| (*ctx.props().default_format).clone())
237
+ }
238
+
239
+ fn set_format(&mut self, ctx: &Context<Self>, format: DatetimeFormatType) {
240
+ self.config.date_format = (format != *ctx.props().default_format).then_some(format);
241
+ }
242
+
204
243
  /// When this config has changed, we must signal the wrapper element.
205
244
  fn dispatch_config(&self, ctx: &Context<Self>) {
206
245
  let value = if self.config == DatetimeColumnStyleConfig::default() {
@@ -17,7 +17,7 @@ use perspective_js::utils::{ApiFuture, JsValueSerdeExt};
17
17
  use wasm_bindgen::prelude::*;
18
18
  use yew::prelude::*;
19
19
 
20
- use crate::components::form::code_editor::CodeEditor;
20
+ use crate::components::code_editor::CodeEditor;
21
21
  use crate::js::{MimeType, copy_to_clipboard, paste_from_clipboard};
22
22
  use crate::presentation::*;
23
23
  use crate::renderer::*;
@@ -169,10 +169,10 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
169
169
  <div id="debug-panel-overflow">
170
170
  <div id="debug-panel" class="sidebar_column" ref={sizer}>
171
171
  <div id="debug-panel-controls">
172
- <button disabled={!*modified} onclick={onapply}>{ "Apply" }</button>
173
- <button disabled={!*modified} onclick={onreset}>{ "Reset" }</button>
174
- <button onclick={oncopy}>{ "Copy" }</button>
175
- <button onclick={onpaste}>{ "Paste" }</button>
172
+ <button id="debug-panel-apply" disabled={!*modified} onclick={onapply} />
173
+ <button id="debug-panel-reset" disabled={!*modified} onclick={onreset} />
174
+ <button id="debug-panel-copy" onclick={oncopy} />
175
+ <button id="debug-panel-paste" onclick={onpaste} />
176
176
  </div>
177
177
  <div id="debug-panel-editor">
178
178
  <CodeEditor
@@ -23,6 +23,7 @@ use crate::components::column_dropdown::ColumnDropDownElement;
23
23
  use crate::components::column_selector::{EmptyColumn, InPlaceColumn, InvalidColumn};
24
24
  use crate::components::type_icon::TypeIcon;
25
25
  use crate::presentation::{DragDropContainer, Presentation};
26
+ use crate::ui::intl_slug;
26
27
  use crate::utils::DragTarget;
27
28
 
28
29
  #[derive(Properties, Derivative)]
@@ -405,16 +406,7 @@ where
405
406
  // here while the var VALUE stays in the stylesheets - custom
406
407
  // properties inherit, so the label's `:before` picks it up.
407
408
  let role_style = ctx.props().role_label.as_ref().map(|role| {
408
- let slug = role
409
- .chars()
410
- .map(|x| {
411
- if x.is_ascii_alphanumeric() {
412
- x.to_ascii_lowercase()
413
- } else {
414
- '-'
415
- }
416
- })
417
- .collect::<String>();
409
+ let slug = intl_slug(role);
418
410
 
419
411
  // The second var is a PRESENCE FLAG: CSS cannot ask whether a
420
412
  // custom property is set, so the slot's stylesheet reads it as
@@ -14,10 +14,10 @@ use std::rc::Rc;
14
14
 
15
15
  use yew::prelude::*;
16
16
 
17
- use super::containers::dropdown_menu::*;
18
17
  use crate::config::*;
19
18
  use crate::renderer::*;
20
19
  use crate::session::Session;
20
+ use crate::ui::{DropDownMenu, DropDownMenuItem};
21
21
 
22
22
  pub type ExportDropDownMenuItem = DropDownMenuItem<ExportFile>;
23
23
 
@@ -15,7 +15,7 @@ use std::rc::Rc;
15
15
  use perspective_client::{ExprValidationError, clone};
16
16
  use yew::prelude::*;
17
17
 
18
- use super::form::code_editor::*;
18
+ use super::code_editor::CodeEditor;
19
19
  use crate::session::{Session, SessionMetadataRc};
20
20
  use crate::tasks::{ExprValidation, validate_expression};
21
21
 
@@ -19,9 +19,9 @@ use web_sys::*;
19
19
  use yew::html::ImplicitClone;
20
20
  use yew::prelude::*;
21
21
 
22
- use super::portal::PortalModal;
23
22
  use super::style::StyleSurface;
24
23
  use crate::session::Session;
24
+ use crate::ui::PortalModal;
25
25
  use crate::utils::*;
26
26
  use crate::*;
27
27
 
@@ -219,7 +219,7 @@ impl Component for FilterDropDownPortal {
219
219
  html! {
220
220
  <PortalModal
221
221
  tag_name="perspective-dropdown"
222
- surface={StyleSurface::FilterDropdown}
222
+ sheet={StyleSurface::FilterDropdown.sheet()}
223
223
  {target}
224
224
  own_focus=false
225
225
  {on_close}
@@ -19,8 +19,8 @@ use web_sys::*;
19
19
  use yew::html::ImplicitClone;
20
20
  use yew::prelude::*;
21
21
 
22
- use super::portal::PortalModal;
23
22
  use super::style::StyleSurface;
23
+ use crate::ui::PortalModal;
24
24
  use crate::utils::*;
25
25
 
26
26
  #[derive(Default)]
@@ -157,7 +157,7 @@ impl Component for FunctionDropDownPortal {
157
157
  html! {
158
158
  <PortalModal
159
159
  tag_name="perspective-dropdown"
160
- surface={StyleSurface::FunctionDropdown}
160
+ sheet={StyleSurface::FunctionDropdown.sheet()}
161
161
  {target}
162
162
  own_focus=false
163
163
  {on_close}
@@ -21,42 +21,6 @@
21
21
  //! own per-panel themed background), but while the plugin is missing — not yet
22
22
  //! mounted, first draw pending, or torn down — the host-theme container shows
23
23
  //! through a panel themed differently, as a visible artifact.
24
- //!
25
- //! There is no native cascade channel from the light-DOM plugin to the frame
26
- //! (siblings in the flattened tree), so the value is mirrored imperatively:
27
- //! each panel's *plugin element* — light-DOM, slotted under the panel's slot,
28
- //! mounted eagerly at panel creation (`create_panel_model` /
29
- //! `Renderer::mount_active_plugin`) — is where the document theme rules
30
- //! actually resolve, so its computed `--psp--background-color` is copied onto
31
- //! the frame's inline style, where the frame's shadow parts inherit it. The
32
- //! plugin is the ONLY sound source element:
33
- //!
34
- //! - An owned hidden probe child of the viewer does NOT work: an *unslotted*
35
- //! light-DOM child of a shadow host is outside the flattened tree, and
36
- //! `getComputedStyle` returns all-empty for it even though the document rules
37
- //! match it syntactically (selector matching is DOM-tree; style computation
38
- //! is flat-tree).
39
- //! - The `<perspective-viewer-tab>` is slotted, but stamps its own `theme` attr
40
- //! in `PanelTab::rendered` — an unordered sibling lifecycle relative to this
41
- //! pass (the theme-stamp-lag class of bug).
42
- //! - The plugin's `theme` attr, by contrast, is stamped *synchronously at the
43
- //! mutation sites* (`update_theme`, `restorePanel`, …) before the events that
44
- //! schedule this render, so it is current when this pass reads it.
45
- //!
46
- //! A read is trusted ONLY when the plugin's stamped attr equals the theme
47
- //! being mirrored: a freshly-created panel's plugin is mounted *unstamped*
48
- //! (the attr first lands inside its first locked dispatch), and reading it
49
- //! early would silently mirror the HOST theme's values. Trusted reads seed
50
- //! [`MainPanel::theme_backgrounds`], the fallback for frames with no readable
51
- //! plugin (an unregistered plugin name stays lazily unmounted; plugin-switch
52
- //! and teardown transients). A frame with neither leaves the pass gate
53
- //! unlatched, so it retries on subsequent renders until the dispatch stamp
54
- //! lands (the render that follows that dispatch's `update_count` bump).
55
- //!
56
- //! Unlike the plugin `theme`/`active` stamps (see the NOTE in [`reconcile`]),
57
- //! the frame is pure viewer chrome — no plugin dispatch reads it — so
58
- //! stamping it from this async `rendered` pass cannot split a plugin draw
59
- //! across paints, and it needs no lock.
60
24
 
61
25
  use wasm_bindgen::prelude::*;
62
26
  use yew::prelude::*;
@@ -69,12 +33,7 @@ use crate::workspace::PanelId;
69
33
  /// `.rl-panel::part(container)` background resolves.
70
34
  const BACKGROUND_VAR: &str = "--psp--background-color";
71
35
 
72
- /// The inputs the frame backgrounds were last computed from; re-mirroring is
73
- /// skipped while these are unchanged, so the forced style recalcs of
74
- /// `getComputedStyle` don't run on every render (e.g. the per-update
75
- /// `update_count` renders of a streaming table). `available_themes` is part of
76
- /// the key because theme CSS registering late changes the *computed* value
77
- /// under an unchanged theme name.
36
+ /// The inputs the frame backgrounds were last computed from.
78
37
  pub(super) type FrameThemeSnapshot = (
79
38
  Vec<PanelId>,
80
39
  Vec<(String, Option<String>)>,
@@ -82,12 +41,7 @@ pub(super) type FrameThemeSnapshot = (
82
41
  );
83
42
 
84
43
  impl MainPanel {
85
- /// Mirror each panel's effective theme background (its own theme, else the
86
- /// registry default — the same fallback the tab/menu use) onto its
87
- /// `<regular-layout-frame>`'s inline `--psp--background-color`, read off
88
- /// the panel's stamped plugin element (else the theme-keyed cache of prior
89
- /// reads). A panel with no resolvable theme has the property removed,
90
- /// falling back to the inherited host-theme value.
44
+ /// Mirror each panel's effective theme background.
91
45
  pub(super) fn stamp_frame_themes(&mut self, ctx: &Context<Self>) {
92
46
  let Some(layout) = self.layout_ref.cast::<web_sys::Element>() else {
93
47
  return;
@@ -104,8 +58,6 @@ impl MainPanel {
104
58
  return;
105
59
  }
106
60
 
107
- // A theme-registry change can re-value an unchanged theme name, so
108
- // cached reads from the previous registry are unsound.
109
61
  if let Some((_, _, prev_themes)) = &self.stamped_frame_themes
110
62
  && prev_themes != &snapshot.2
111
63
  {
@@ -155,19 +107,11 @@ impl MainPanel {
155
107
  }
156
108
  }
157
109
 
158
- // Latch only a fully-resolved pass; an unresolved frame (unmounted or
159
- // not-yet-stamped plugin) retries each render until its dispatch
160
- // stamp lands.
161
110
  self.stamped_frame_themes = complete.then_some(snapshot);
162
111
  }
163
112
  }
164
113
 
165
- /// Read the computed [`BACKGROUND_VAR`] off `slot`'s plugin element — the
166
- /// viewer light-DOM child mounted under exactly that slot name (the tab and
167
- /// toolbar use `tab-`/`statusbar-extra-` prefixed slots). Requires the
168
- /// plugin's `theme` attr to already equal `theme` — during a plugin switch
169
- /// two elements briefly share the slot, and a freshly-mounted plugin is not
170
- /// yet stamped; both are disambiguated by the attr check.
114
+ /// Read the computed [`BACKGROUND_VAR`] off `slot`'s plugin element.
171
115
  fn read_plugin_background(viewer: &web_sys::Element, slot: &str, theme: &str) -> Option<String> {
172
116
  let children = viewer.children();
173
117
  let plugin = (0..children.length())
@@ -190,16 +190,21 @@ impl MainPanel {
190
190
  self.inserted.push(name.to_owned());
191
191
  }
192
192
 
193
- // Remove cells whose panels are gone.
193
+ let mut pending = std::mem::take(&mut self.pending_removals);
194
194
  self.inserted.retain(|name| {
195
195
  if panel_ids.iter().any(|id| id.as_str() == name) {
196
- true
197
- } else {
196
+ return true;
197
+ }
198
+
199
+ if pending.insert(name.clone()) {
198
200
  let _ = layout.remove_panel(name);
199
- false
200
201
  }
202
+
203
+ true
201
204
  });
202
205
 
206
+ self.pending_removals = pending;
207
+
203
208
  // A removed panel can't stay "maximized" (regular-layout drops the
204
209
  // maximize stylesheet when its panel leaves the layout).
205
210
  if let Some(m) = &self.maximized
@@ -18,7 +18,8 @@ use wasm_bindgen::prelude::*;
18
18
  use yew::prelude::*;
19
19
 
20
20
  use super::{MainPanel, MainPanelMsg, MainPanelProps};
21
- use crate::components::panel_menu::PanelMenu;
21
+ use crate::components::new_panel_menu::NewPanelPick;
22
+ use crate::components::panel_menu::{PanelCommand, PanelMenu};
22
23
  use crate::components::panel_tab::PanelTab;
23
24
  use crate::components::render_warning::RenderWarning;
24
25
  use crate::components::status_bar::StatusBar;
@@ -73,14 +74,26 @@ impl MainPanel {
73
74
  let active_slot = renderer.slot_name();
74
75
  // Panel-count chrome (`single`/`multi`, closable, draggable)
75
76
  let placed_count = ctx.props().workspace.placed_count();
77
+ let closing = self
78
+ .inserted
79
+ .iter()
80
+ .filter(|name| !ctx.props().panel_ids.iter().any(|id| id.as_str() == *name))
81
+ .map(|name| PanelId::from(name.as_str()))
82
+ .collect::<Vec<_>>();
76
83
  let cells = ctx
77
84
  .props()
78
85
  .panel_ids
79
86
  .iter()
80
87
  .filter(|id| !ctx.props().workspace.is_staged(id))
81
- .map(|id| {
88
+ .map(|id| (id, false))
89
+ .chain(closing.iter().map(|id| (id, true)))
90
+ .map(|(id, is_closing)| {
82
91
  let name = id.as_str().to_owned();
83
92
  let mut class = classes!("rl-panel");
93
+ if is_closing {
94
+ class.push("closing");
95
+ }
96
+
84
97
  if placed_count > 1 {
85
98
  if active_slot.as_deref() == Some(name.as_str()) {
86
99
  class.push("active");
@@ -103,7 +116,9 @@ impl MainPanel {
103
116
  let on_activate = ctx.props().on_activate_panel.clone();
104
117
  let activate_name = name.clone();
105
118
  let onpointerdown = Callback::from(move |_: web_sys::PointerEvent| {
106
- on_activate.emit(activate_name.clone());
119
+ if !is_closing {
120
+ on_activate.emit(activate_name.clone());
121
+ }
107
122
  });
108
123
 
109
124
  // Right-click anywhere in the panel opens the context menu. This
@@ -214,23 +229,41 @@ impl MainPanel {
214
229
  on_settings.emit(());
215
230
  })
216
231
  };
232
+ let closing_tabs = closing.iter().filter_map(|id| {
233
+ let panel = ctx.props().workspace.closing_panel(id)?;
234
+ let title = panel.session.get_title().filter(|t| !t.is_empty());
235
+ let theme = panel.renderer.theme().or_else(|| {
236
+ ctx.props()
237
+ .presentation_props
238
+ .available_themes
239
+ .first()
240
+ .cloned()
241
+ });
242
+ Some((id, panel.session, title, theme, true))
243
+ });
217
244
  let tabs = ctx
218
245
  .props()
219
246
  .panel_ids
220
247
  .iter()
221
248
  .filter(|id| !ctx.props().workspace.is_staged(id))
222
- .map(|id| {
223
- let name = id.as_str().to_owned();
249
+ .filter_map(|id| {
250
+ let panel = ctx.props().workspace.panel(id)?;
251
+ let name = id.as_str();
224
252
  let title = ctx
225
253
  .props()
226
254
  .panel_titles
227
255
  .iter()
228
- .find(|(pid, _)| pid == &name)
256
+ .find(|(pid, _)| pid == name)
229
257
  .and_then(|(_, t)| t.clone());
230
- let active = active_slot.as_deref() == Some(name.as_str());
258
+ let theme = ctx.props().effective_panel_theme(name);
259
+ Some((id, panel.session, title, theme, false))
260
+ })
261
+ .chain(closing_tabs)
262
+ .map(|(id, session, title, theme, is_closing)| {
263
+ let name = id.as_str().to_owned();
264
+ let active = !is_closing && active_slot.as_deref() == Some(name.as_str());
231
265
  let visible = !self.hidden_tabs.contains(name.as_str());
232
- let is_master = ctx.props().panel_masters.contains(id);
233
- let theme = ctx.props().effective_panel_theme(&name);
266
+ let is_master = !is_closing && ctx.props().panel_masters.contains(id);
234
267
 
235
268
  html! {
236
269
  <PanelTab
@@ -238,14 +271,16 @@ impl MainPanel {
238
271
  viewer={viewer_elem.clone()}
239
272
  panel_id={name.clone()}
240
273
  {title}
274
+ {session}
241
275
  {theme}
242
276
  {active}
243
277
  {visible}
244
278
  {is_master}
245
279
  {single}
246
- {closable}
280
+ closable={closable && !is_closing}
247
281
  {is_settings_open}
248
- {draggable}
282
+ draggable={draggable && !is_closing}
283
+ closing={is_closing}
249
284
  on_select={on_select.clone()}
250
285
  on_close={on_close.clone()}
251
286
  on_open_settings={on_open_settings.clone()}
@@ -349,11 +384,27 @@ impl MainPanel {
349
384
  })
350
385
  .unwrap_or_default();
351
386
 
387
+ let on_new_panel = {
388
+ let on_command = ctx.props().on_panel_command.clone();
389
+ let id = renderer.slot_name().unwrap_or_default();
390
+ Callback::from(move |pick: NewPanelPick| {
391
+ let cmd = match pick {
392
+ NewPanelPick::FromTable { client, table } => {
393
+ PanelCommand::NewFrom { client, table }
394
+ },
395
+ NewPanelPick::FromPanel(source) => PanelCommand::NewFromPanel(source),
396
+ };
397
+
398
+ on_command.emit((id.clone(), cmd));
399
+ })
400
+ };
401
+
352
402
  html! {
353
403
  <div id="main_column">
354
404
  <StatusBar
355
405
  id="status_bar"
356
406
  on_reset={ctx.props().on_reset.clone()}
407
+ {on_new_panel}
357
408
  session_props={ctx.props().session_props.clone()}
358
409
  presentation_props={ctx.props().presentation_props.clone()}
359
410
  is_settings_open={ctx.props().is_settings_open}
@@ -22,6 +22,7 @@ use yew::prelude::*;
22
22
  use super::MainPanel;
23
23
  use crate::components::panel_menu::PanelCommand;
24
24
  use crate::js::{Layout, RegularLayout};
25
+ use crate::renderer::SUBPIXEL_EPSILON;
25
26
  use crate::tasks::resize_callback;
26
27
 
27
28
  impl MainPanel {
@@ -42,11 +43,9 @@ impl MainPanel {
42
43
  false
43
44
  }
44
45
 
46
+ /// Report closed every panel this commit dropped from the layout, and
47
+ /// re-render so its frame leaves the DOM in the commit's paint.
45
48
  pub(super) fn on_layout_updated(&mut self, ctx: &Context<Self>) -> bool {
46
- // A panel still tracked in `inserted` but no longer present in
47
- // the layout (calculatePath → null) was removed by the layout —
48
- // i.e. its frame's close button was pressed. Report it for
49
- // disposal.
50
49
  let Some(el) = self.layout_ref.cast::<web_sys::HtmlElement>() else {
51
50
  return false;
52
51
  };
@@ -68,10 +67,14 @@ impl MainPanel {
68
67
  }
69
68
  });
70
69
 
70
+ let closed_any = !closed.is_empty();
71
71
  for id in closed {
72
- ctx.props().on_close_panel.emit(id);
72
+ self.pending_removals.remove(&id);
73
+ ctx.props().on_panel_closed.emit(id);
73
74
  }
74
75
 
76
+ let reissue = self.forget_dropped_removals(&layout);
77
+
75
78
  // A layout change (divider drag, insert, restore) reflows the
76
79
  // grid cells without resizing the host, so the host
77
80
  // `ResizeObserver` never fires. Resize every activated panel's
@@ -92,6 +95,19 @@ impl MainPanel {
92
95
  if let Some(panel) = ctx.props().workspace.panel(id)
93
96
  && panel.renderer.is_plugin_activated().unwrap_or(false)
94
97
  {
98
+ if let Some((w, h)) = panel.renderer.take_presized_box()
99
+ && let Some(plugin) = panel.renderer.active_plugin()
100
+ {
101
+ let rect = plugin
102
+ .unchecked_ref::<web_sys::Element>()
103
+ .get_bounding_client_rect();
104
+ if (rect.width() - w).abs() <= SUBPIXEL_EPSILON
105
+ && (rect.height() - h).abs() <= SUBPIXEL_EPSILON
106
+ {
107
+ continue;
108
+ }
109
+ }
110
+
95
111
  resize_callback(&panel.session, &panel.renderer).emit(());
96
112
  }
97
113
  }
@@ -111,10 +127,27 @@ impl MainPanel {
111
127
  self.hidden_tabs = hidden;
112
128
  true
113
129
  } else {
114
- false
130
+ closed_any || reissue
115
131
  }
116
132
  }
117
133
 
134
+ /// Forget every issued removal whose panel survived the commit, so the
135
+ /// next `reconcile` re-issues it.
136
+ fn forget_dropped_removals(&mut self, layout: &RegularLayout) -> bool {
137
+ let dropped = self
138
+ .pending_removals
139
+ .iter()
140
+ .filter(|id| layout.contains_panel(id))
141
+ .cloned()
142
+ .collect::<Vec<_>>();
143
+ let reissue = !dropped.is_empty();
144
+ for id in dropped {
145
+ self.pending_removals.remove(&id);
146
+ }
147
+
148
+ reissue
149
+ }
150
+
118
151
  pub(super) fn on_tab_selected(&self, ctx: &Context<Self>, name: String) -> bool {
119
152
  ctx.props().on_activate_panel.emit(name);
120
153
  false
@@ -169,10 +202,6 @@ impl MainPanel {
169
202
 
170
203
  self.maximized = None;
171
204
  },
172
- // The stage menu (no target panel) offers ONLY `NewFrom`, whose
173
- // handler resolves the client/table purely from the loaded-clients
174
- // registry — the panel id is unused there, so the empty id never
175
- // reaches a panel lookup.
176
205
  cmd => ctx
177
206
  .props()
178
207
  .on_panel_command