@perspective-dev/viewer 5.2.0 → 5.3.1

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 (179) 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/de.css +1 -1
  11. package/dist/css/intl/es.css +1 -1
  12. package/dist/css/intl/fr.css +1 -1
  13. package/dist/css/intl/ja.css +1 -1
  14. package/dist/css/intl/pt.css +1 -1
  15. package/dist/css/intl/zh.css +1 -1
  16. package/dist/css/intl.css +1 -1
  17. package/dist/css/ledger.css +1 -0
  18. package/dist/css/monokai.css +1 -1
  19. package/dist/css/nord.css +1 -0
  20. package/dist/css/phosphor.css +1 -1
  21. package/dist/css/pro-dark.css +1 -1
  22. package/dist/css/pro.css +1 -1
  23. package/dist/css/solarized-dark.css +1 -1
  24. package/dist/css/solarized.css +1 -1
  25. package/dist/css/themes.css +1 -1
  26. package/dist/css/vaporwave.css +1 -1
  27. package/dist/css/velvet.css +1 -0
  28. package/dist/docs/perspective-docs.json +1 -1
  29. package/dist/esm/column-format.d.ts +16 -3
  30. package/dist/esm/perspective-viewer.d.ts +1 -0
  31. package/dist/esm/perspective-viewer.inline.js +9 -9
  32. package/dist/esm/perspective-viewer.inline.js.map +3 -3
  33. package/dist/esm/perspective-viewer.js +8 -8
  34. package/dist/esm/perspective-viewer.js.map +3 -3
  35. package/dist/esm/plugin.d.ts +10 -0
  36. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +1 -1
  37. package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
  38. package/dist/esm/ts-rs/ViewerConfig.d.ts +1 -1
  39. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  40. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +13 -3
  41. package/dist/wasm/perspective-viewer.d.ts +43 -36
  42. package/dist/wasm/perspective-viewer.js +198 -136
  43. package/dist/wasm/perspective-viewer.wasm +0 -0
  44. package/dist/wasm/perspective-viewer.wasm.d.ts +19 -17
  45. package/package.json +1 -1
  46. package/src/css/column-settings-panel.css +28 -5
  47. package/src/css/column-style.css +291 -31
  48. package/src/css/containers/tabs.css +13 -3
  49. package/src/css/dom/select.css +21 -9
  50. package/src/css/form/code-editor.css +4 -4
  51. package/src/css/plugin-selector.css +1 -0
  52. package/src/css/plugin-settings-panel.css +72 -0
  53. package/src/css/viewer.css +0 -5
  54. package/src/rust/agent/tools.rs +53 -19
  55. package/src/rust/components/column_selector/active_column.rs +2 -2
  56. package/src/rust/components/column_selector/add_expression_button.rs +3 -3
  57. package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
  58. package/src/rust/components/column_selector/config_selector.rs +8 -0
  59. package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
  60. package/src/rust/components/column_selector/inactive_column.rs +2 -2
  61. package/src/rust/components/column_selector.rs +48 -4
  62. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
  63. package/src/rust/components/column_settings_sidebar/style_tab.rs +330 -234
  64. package/src/rust/components/column_settings_sidebar.rs +88 -69
  65. package/src/rust/components/containers/control_group.rs +91 -0
  66. package/src/rust/components/containers/mod.rs +1 -0
  67. package/src/rust/components/containers/select.rs +8 -2
  68. package/src/rust/components/datetime_column_style/simple.rs +15 -4
  69. package/src/rust/components/datetime_column_style.rs +50 -10
  70. package/src/rust/components/editable_header.rs +85 -144
  71. package/src/rust/components/expression_editor.rs +9 -16
  72. package/src/rust/components/form/code_editor.rs +16 -6
  73. package/src/rust/components/form/debug.rs +1 -0
  74. package/src/rust/components/form/mod.rs +3 -1
  75. package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
  76. package/src/rust/components/form/named_value_picker.rs +86 -0
  77. package/src/rust/components/form/palette_selector.rs +440 -0
  78. package/src/rust/components/main_panel/msg.rs +2 -2
  79. package/src/rust/components/main_panel/reconcile.rs +4 -4
  80. package/src/rust/components/plugin_tab.rs +126 -112
  81. package/src/rust/components/settings_panel.rs +26 -20
  82. package/src/rust/components/status_bar.rs +0 -1
  83. package/src/rust/components/style_controls/number_string_format/digits_section.rs +22 -10
  84. package/src/rust/components/style_controls/number_string_format/misc_section.rs +6 -2
  85. package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -0
  86. package/src/rust/components/style_controls/number_string_format/types.rs +24 -0
  87. package/src/rust/components/style_controls/number_string_format.rs +37 -14
  88. package/src/rust/components/viewer/msg.rs +46 -7
  89. package/src/rust/components/viewer/panels.rs +11 -9
  90. package/src/rust/components/viewer/render.rs +8 -3
  91. package/src/rust/components/viewer/settings.rs +163 -23
  92. package/src/rust/components/viewer/snapshots.rs +46 -21
  93. package/src/rust/components/viewer/wiring.rs +14 -25
  94. package/src/rust/components/viewer.rs +12 -8
  95. package/src/rust/config/column_config_schema.rs +435 -45
  96. package/src/rust/config/css_scale.rs +882 -0
  97. package/src/rust/config/datetime_column_style.rs +4 -21
  98. package/src/rust/config/mod.rs +2 -0
  99. package/src/rust/config/number_string_format.rs +211 -18
  100. package/src/rust/config/options.rs +10 -0
  101. package/src/rust/config/viewer_config.rs +49 -55
  102. package/src/rust/config/workspace_config.rs +132 -5
  103. package/src/rust/custom_elements/viewer.rs +214 -155
  104. package/src/rust/custom_events.rs +14 -1
  105. package/src/rust/js/regular_layout.rs +3 -3
  106. package/src/rust/lib.rs +2 -0
  107. package/src/rust/presentation/column_locator.rs +19 -5
  108. package/src/rust/presentation.rs +209 -36
  109. package/src/rust/queries/column_locator.rs +74 -44
  110. package/src/rust/queries/get_viewer_config.rs +0 -3
  111. package/src/rust/queries/mod.rs +2 -0
  112. package/src/rust/queries/palette_set.rs +82 -0
  113. package/src/rust/queries/plugin_column_styles.rs +6 -2
  114. package/src/rust/renderer/dispatch.rs +50 -12
  115. package/src/rust/renderer/plugin_config.rs +362 -53
  116. package/src/rust/renderer.rs +15 -42
  117. package/src/rust/session/column_defaults_update.rs +15 -8
  118. package/src/rust/session/metadata.rs +5 -1
  119. package/src/rust/session/props.rs +1 -0
  120. package/src/rust/session.rs +9 -6
  121. package/src/rust/tasks/create_panel.rs +44 -57
  122. package/src/rust/tasks/edit_expression.rs +19 -23
  123. package/src/rust/tasks/edit_window.rs +8 -10
  124. package/src/rust/tasks/eject.rs +4 -1
  125. package/src/rust/tasks/pipeline.rs +2 -2
  126. package/src/rust/tasks/presize_panels.rs +44 -5
  127. package/src/rust/tasks/reset_all.rs +4 -4
  128. package/src/rust/tasks/restore_and_render.rs +17 -44
  129. package/src/rust/tasks/restore_panel.rs +11 -13
  130. package/src/rust/tasks/set_edit_mode.rs +3 -1
  131. package/src/rust/tasks/sync_update_panels.rs +11 -1
  132. package/src/rust/tasks/update_theme.rs +23 -18
  133. package/src/rust/utils/css_vars.rs +26 -0
  134. package/src/rust/utils/mod.rs +2 -0
  135. package/src/rust/utils/spawn.rs +1 -1
  136. package/src/rust/workspace.rs +435 -272
  137. package/src/svg/lock.svg +7 -0
  138. package/src/themes/blueprint.css +190 -0
  139. package/src/themes/botanical.css +14 -17
  140. package/src/themes/defaults.css +4 -110
  141. package/src/themes/dracula.css +19 -14
  142. package/src/themes/eggplant.css +189 -0
  143. package/src/themes/gruvbox-dark.css +23 -15
  144. package/src/themes/gruvbox.css +11 -1
  145. package/src/themes/icons.css +2 -2
  146. package/src/themes/intl/de.css +23 -1
  147. package/src/themes/intl/es.css +23 -1
  148. package/src/themes/intl/fr.css +23 -1
  149. package/src/themes/intl/ja.css +23 -1
  150. package/src/themes/intl/pt.css +23 -1
  151. package/src/themes/intl/zh.css +23 -1
  152. package/src/themes/intl.css +26 -4
  153. package/src/themes/ledger.css +195 -0
  154. package/src/themes/monokai.css +24 -14
  155. package/src/themes/nord.css +186 -0
  156. package/src/themes/phosphor.css +7 -14
  157. package/src/themes/pro-dark.css +12 -16
  158. package/src/themes/pro.css +118 -0
  159. package/src/themes/solarized-dark.css +9 -16
  160. package/src/themes/solarized.css +7 -0
  161. package/src/themes/themes.css +10 -5
  162. package/src/themes/vaporwave.css +22 -14
  163. package/src/themes/velvet.css +192 -0
  164. package/src/ts/column-format.ts +52 -2
  165. package/src/ts/perspective-viewer.ts +1 -0
  166. package/src/ts/plugin.ts +14 -0
  167. package/src/ts/ts-rs/PanelViewerConfig.ts +1 -1
  168. package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
  169. package/src/ts/ts-rs/ViewerConfig.ts +1 -1
  170. package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
  171. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +11 -3
  172. package/src/rust/components/form/color_range_selector.rs +0 -137
  173. package/src/svg/dropdown-selector-light.svg +0 -3
  174. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline0.js +0 -0
  175. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline1.js +0 -0
  176. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline2.js +0 -0
  177. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline3.js +0 -0
  178. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline4.js +0 -0
  179. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline5.js +0 -0
@@ -0,0 +1,440 @@
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 wasm_bindgen::JsCast;
14
+ use web_sys::*;
15
+ use yew::prelude::*;
16
+
17
+ /// The widget for [`crate::config::ControlSpec::Palette`]: an ordered row
18
+ /// of discrete color swatches with per-swatch edit/remove, pointer-drag
19
+ /// reordering, and a trailing add tile.
20
+ #[derive(Properties, PartialEq)]
21
+ pub struct PaletteProps {
22
+ pub values: Vec<String>,
23
+ pub on_change: Callback<Vec<String>>,
24
+ pub on_reset: Callback<()>,
25
+ pub is_modified: bool,
26
+
27
+ #[prop_or_default]
28
+ pub max: Option<usize>,
29
+
30
+ #[prop_or_default]
31
+ pub title: Option<String>,
32
+ }
33
+
34
+ const DRAG_THRESHOLD_PX: f64 = 4.0;
35
+
36
+ #[derive(Clone, Copy)]
37
+ struct Press {
38
+ pointer_id: i32,
39
+ x: i32,
40
+ y: i32,
41
+ from: usize,
42
+ }
43
+
44
+ fn event_index(target: &Element) -> Option<usize> {
45
+ target
46
+ .get_attribute("data-index")
47
+ .and_then(|x| x.parse::<usize>().ok())
48
+ }
49
+
50
+ fn insertion_index(centers: &[(f64, f64)], x: f64, y: f64) -> usize {
51
+ let nearest = centers
52
+ .iter()
53
+ .enumerate()
54
+ .min_by(|(_, a), (_, b)| {
55
+ let da = (a.0 - x).hypot(a.1 - y);
56
+ let db = (b.0 - x).hypot(b.1 - y);
57
+ da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
58
+ })
59
+ .map(|(index, _)| index);
60
+
61
+ match nearest {
62
+ Some(index) if x < centers[index].0 => index,
63
+ Some(index) => index + 1,
64
+ None => 0,
65
+ }
66
+ }
67
+
68
+ fn reordered<T: Clone>(values: &[T], from: usize, insert: usize) -> Option<Vec<T>> {
69
+ if from >= values.len() || insert > values.len() || insert == from || insert == from + 1 {
70
+ return None;
71
+ }
72
+
73
+ let mut next = values.to_vec();
74
+ let color = next.remove(from);
75
+ let at = if insert > from { insert - 1 } else { insert };
76
+ next.insert(at, color);
77
+ Some(next)
78
+ }
79
+
80
+ fn slot_from_display(slot: usize, shown_at: usize, from: usize) -> usize {
81
+ let shown = if slot > shown_at { slot - 1 } else { slot };
82
+ match shown.cmp(&from) {
83
+ std::cmp::Ordering::Less => shown,
84
+ std::cmp::Ordering::Greater => shown + 1,
85
+ std::cmp::Ordering::Equal => from,
86
+ }
87
+ }
88
+
89
+ fn swatch_centers(container: &Element) -> Vec<(f64, f64, usize)> {
90
+ let Ok(nodes) = container.query_selector_all(".palette-swatch") else {
91
+ return vec![];
92
+ };
93
+
94
+ (0..nodes.length())
95
+ .filter_map(|i| nodes.get(i))
96
+ .filter_map(|node| node.dyn_into::<Element>().ok())
97
+ .filter_map(|el| {
98
+ let index = event_index(&el)?;
99
+ let rect = el.get_bounding_client_rect();
100
+ Some((
101
+ rect.left() + rect.width() / 2.0,
102
+ rect.top() + rect.height() / 2.0,
103
+ index,
104
+ ))
105
+ })
106
+ .collect()
107
+ }
108
+
109
+ #[function_component(PaletteSelector)]
110
+ pub fn palette_selector(props: &PaletteProps) -> Html {
111
+ let oninput = use_callback(
112
+ (props.values.clone(), props.on_change.clone()),
113
+ |event: InputEvent, (values, on_change)| {
114
+ let input = event.target().unwrap().unchecked_into::<HtmlInputElement>();
115
+
116
+ let Some(index) = event_index(&input) else {
117
+ return;
118
+ };
119
+
120
+ let mut next = values.clone();
121
+ let Some(slot) = next.get_mut(index) else {
122
+ return;
123
+ };
124
+
125
+ *slot = input.value();
126
+ on_change.emit(next);
127
+ },
128
+ );
129
+
130
+ let on_remove = use_callback(
131
+ (props.values.clone(), props.on_change.clone()),
132
+ |event: MouseEvent, (values, on_change)| {
133
+ let Some(target) = event.target().and_then(|x| x.dyn_into::<Element>().ok()) else {
134
+ return;
135
+ };
136
+
137
+ let Some(index) = event_index(&target) else {
138
+ return;
139
+ };
140
+
141
+ if values.len() > 1 && index < values.len() {
142
+ let mut next = values.clone();
143
+ next.remove(index);
144
+ on_change.emit(next);
145
+ }
146
+ },
147
+ );
148
+
149
+ let swatches_ref = use_node_ref();
150
+ let open_index = use_mut_ref(|| None::<usize>);
151
+ let on_add = use_callback(
152
+ (
153
+ props.values.clone(),
154
+ props.on_change.clone(),
155
+ props.max,
156
+ open_index.clone(),
157
+ ),
158
+ |_: MouseEvent, (values, on_change, max, open_index)| {
159
+ if max.map(|max| values.len() < max).unwrap_or(true)
160
+ && let Some(last) = values.last()
161
+ {
162
+ let mut next = values.clone();
163
+ next.push(last.clone());
164
+ *open_index.borrow_mut() = Some(values.len());
165
+ on_change.emit(next);
166
+ }
167
+ },
168
+ );
169
+
170
+ {
171
+ let swatches_ref = swatches_ref.clone();
172
+ let open_index = open_index.clone();
173
+ use_effect(move || {
174
+ let Some(index) = *open_index.borrow() else {
175
+ return;
176
+ };
177
+
178
+ let input = swatches_ref
179
+ .cast::<Element>()
180
+ .and_then(|el| {
181
+ el.query_selector(&format!("input[data-index=\"{index}\"]"))
182
+ .ok()
183
+ .flatten()
184
+ })
185
+ .and_then(|el| el.dyn_into::<HtmlInputElement>().ok());
186
+
187
+ if let Some(input) = input {
188
+ *open_index.borrow_mut() = None;
189
+ let _ = input.focus();
190
+ if input.show_picker().is_err() {
191
+ input.click();
192
+ }
193
+ }
194
+ });
195
+ }
196
+
197
+ let press = use_mut_ref(|| None::<Press>);
198
+ let drag = use_state(|| None::<(usize, usize)>);
199
+ let suppress_click = use_mut_ref(|| false);
200
+
201
+ let on_pointerdown = {
202
+ let press = press.clone();
203
+ let suppress_click = suppress_click.clone();
204
+ Callback::from(move |event: PointerEvent| {
205
+ if event.button() != 0 {
206
+ return;
207
+ }
208
+
209
+ let Some(target) = event.target().and_then(|x| x.dyn_into::<Element>().ok()) else {
210
+ return;
211
+ };
212
+
213
+ if target.class_list().contains("palette-swatch-remove") {
214
+ return;
215
+ }
216
+
217
+ let Some(swatch) = target.closest(".palette-swatch").ok().flatten() else {
218
+ return;
219
+ };
220
+
221
+ let Some(from) = event_index(&swatch) else {
222
+ return;
223
+ };
224
+
225
+ *suppress_click.borrow_mut() = false;
226
+ *press.borrow_mut() = Some(Press {
227
+ pointer_id: event.pointer_id(),
228
+ x: event.client_x(),
229
+ y: event.client_y(),
230
+ from,
231
+ });
232
+ })
233
+ };
234
+
235
+ let on_pointermove = {
236
+ let press = press.clone();
237
+ let drag = drag.clone();
238
+ let swatches_ref = swatches_ref.clone();
239
+ let suppress_click = suppress_click.clone();
240
+ Callback::from(move |event: PointerEvent| {
241
+ let Some(pressed) = *press.borrow() else {
242
+ return;
243
+ };
244
+
245
+ if pressed.pointer_id != event.pointer_id() {
246
+ return;
247
+ }
248
+
249
+ let Some(container) = swatches_ref.cast::<Element>() else {
250
+ return;
251
+ };
252
+
253
+ if drag.is_none() {
254
+ let dx = (event.client_x() - pressed.x) as f64;
255
+ let dy = (event.client_y() - pressed.y) as f64;
256
+ if dx.hypot(dy) < DRAG_THRESHOLD_PX {
257
+ return;
258
+ }
259
+
260
+ let _ = container.set_pointer_capture(pressed.pointer_id);
261
+ *suppress_click.borrow_mut() = true;
262
+ }
263
+
264
+ let shown = swatch_centers(&container);
265
+ let Some(shown_at) = shown
266
+ .iter()
267
+ .position(|(_, _, index)| *index == pressed.from)
268
+ else {
269
+ return;
270
+ };
271
+
272
+ let centers = shown.iter().map(|(x, y, _)| (*x, *y)).collect::<Vec<_>>();
273
+ let slot = insertion_index(&centers, event.client_x() as f64, event.client_y() as f64);
274
+ let insert = slot_from_display(slot, shown_at, pressed.from);
275
+ if *drag != Some((pressed.from, insert)) {
276
+ drag.set(Some((pressed.from, insert)));
277
+ }
278
+ })
279
+ };
280
+
281
+ let on_pointerup = {
282
+ let press = press.clone();
283
+ let drag = drag.clone();
284
+ let values = props.values.clone();
285
+ let on_change = props.on_change.clone();
286
+ Callback::from(move |event: PointerEvent| {
287
+ let pressed = press
288
+ .borrow()
289
+ .is_some_and(|p| p.pointer_id == event.pointer_id());
290
+
291
+ if !pressed {
292
+ return;
293
+ }
294
+
295
+ *press.borrow_mut() = None;
296
+ if let Some((from, insert)) = *drag {
297
+ drag.set(None);
298
+ if let Some(next) = reordered(&values, from, insert) {
299
+ on_change.emit(next);
300
+ }
301
+ }
302
+ })
303
+ };
304
+
305
+ let on_pointercancel = {
306
+ let press = press.clone();
307
+ let drag = drag.clone();
308
+ Callback::from(move |_: PointerEvent| {
309
+ *press.borrow_mut() = None;
310
+ if drag.is_some() {
311
+ drag.set(None);
312
+ }
313
+ })
314
+ };
315
+
316
+ let on_swatch_click = {
317
+ let suppress_click = suppress_click.clone();
318
+ Callback::from(move |event: MouseEvent| {
319
+ if std::mem::replace(&mut *suppress_click.borrow_mut(), false) {
320
+ event.prevent_default();
321
+ }
322
+ })
323
+ };
324
+
325
+ let on_reset = use_callback(props.on_reset.clone(), |_: MouseEvent, on_reset| {
326
+ on_reset.emit(())
327
+ });
328
+
329
+ let drag_from = drag.map(|(from, _)| from);
330
+ let identity = (0..props.values.len()).collect::<Vec<_>>();
331
+ let display = (*drag)
332
+ .and_then(|(from, insert)| reordered(&identity, from, insert))
333
+ .unwrap_or(identity);
334
+
335
+ let can_add = props
336
+ .max
337
+ .map(|max| props.values.len() < max)
338
+ .unwrap_or(true);
339
+ let can_remove = props.values.len() > 1;
340
+
341
+ html! {
342
+ <>
343
+ <label id={props.title.as_deref().unwrap_or("palette-label").to_owned()} />
344
+ <div class="color-gradient-container palette-selector">
345
+ <div
346
+ class={classes!("palette-swatches", drag.is_some().then_some("reordering"))}
347
+ ref={swatches_ref}
348
+ onpointerdown={on_pointerdown}
349
+ onpointermove={on_pointermove}
350
+ onpointerup={on_pointerup}
351
+ onpointercancel={on_pointercancel}
352
+ >
353
+ { for display.iter().map(|&index| html! {
354
+ <div
355
+ class={classes!(
356
+ "palette-swatch",
357
+ (drag_from == Some(index)).then_some("dragging"),
358
+ )}
359
+ key={index.to_string()}
360
+ data-index={index.to_string()}
361
+ >
362
+ <input
363
+ class="parameter"
364
+ type="color"
365
+ value={props.values[index].clone()}
366
+ data-index={index.to_string()}
367
+ oninput={oninput.clone()}
368
+ onclick={on_swatch_click.clone()}
369
+ />
370
+ if can_remove {
371
+ <span
372
+ class="palette-swatch-remove"
373
+ data-index={index.to_string()}
374
+ onclick={on_remove.clone()}
375
+ />
376
+ }
377
+ </div>
378
+ }) }
379
+ if can_add { <span class="palette-add" onclick={on_add} /> }
380
+ </div>
381
+ if props.is_modified {
382
+ <span class="reset-default-style" onclick={on_reset} />
383
+ } else {
384
+ <span class="reset-default-style-disabled" />
385
+ }
386
+ </div>
387
+ </>
388
+ }
389
+ }
390
+
391
+ #[cfg(test)]
392
+ mod tests {
393
+ use super::*;
394
+
395
+ fn v(items: &[&str]) -> Vec<String> {
396
+ items.iter().map(|x| x.to_string()).collect()
397
+ }
398
+
399
+ #[test]
400
+ fn insertion_index_splits_at_centers_and_resolves_rows() {
401
+ let centers = [(12.0, 12.0), (40.0, 12.0), (68.0, 12.0), (12.0, 40.0)];
402
+ assert_eq!(insertion_index(&centers, 0.0, 12.0), 0);
403
+ assert_eq!(insertion_index(&centers, 13.0, 12.0), 1);
404
+ assert_eq!(insertion_index(&centers, 39.0, 12.0), 1);
405
+ assert_eq!(insertion_index(&centers, 41.0, 12.0), 2);
406
+ assert_eq!(insertion_index(&centers, 90.0, 12.0), 3);
407
+ assert_eq!(insertion_index(&centers, 10.0, 41.0), 3);
408
+ assert_eq!(insertion_index(&centers, 30.0, 41.0), 4);
409
+ assert_eq!(insertion_index(&[], 5.0, 5.0), 0);
410
+ }
411
+
412
+ #[test]
413
+ fn slot_from_display_maps_previewed_slots_back_to_original_order() {
414
+ assert_eq!(slot_from_display(3, 2, 0), 3);
415
+ assert_eq!(slot_from_display(2, 2, 0), 3);
416
+ assert_eq!(slot_from_display(1, 2, 0), 2);
417
+ assert_eq!(slot_from_display(0, 2, 0), 0);
418
+ assert_eq!(slot_from_display(0, 0, 2), 0);
419
+ assert_eq!(slot_from_display(1, 0, 2), 0);
420
+ assert_eq!(slot_from_display(2, 0, 2), 1);
421
+ assert_eq!(slot_from_display(3, 0, 2), 2);
422
+ assert_eq!(slot_from_display(4, 0, 2), 4);
423
+ assert_eq!(slot_from_display(1, 1, 1), 1);
424
+ assert_eq!(slot_from_display(2, 1, 1), 1);
425
+ assert_eq!(slot_from_display(3, 1, 1), 3);
426
+ }
427
+
428
+ #[test]
429
+ fn reordered_moves_and_rejects_no_ops() {
430
+ let values = v(&["a", "b", "c", "d"]);
431
+ assert_eq!(reordered(&values, 0, 4), Some(v(&["b", "c", "d", "a"])));
432
+ assert_eq!(reordered(&values, 3, 0), Some(v(&["d", "a", "b", "c"])));
433
+ assert_eq!(reordered(&values, 0, 2), Some(v(&["b", "a", "c", "d"])));
434
+ assert_eq!(reordered(&values, 2, 1), Some(v(&["a", "c", "b", "d"])));
435
+ assert_eq!(reordered(&values, 1, 1), None);
436
+ assert_eq!(reordered(&values, 1, 2), None);
437
+ assert_eq!(reordered(&values, 4, 0), None);
438
+ assert_eq!(reordered(&values, 0, 5), None);
439
+ }
440
+ }
@@ -16,8 +16,8 @@ use crate::components::panel_menu::PanelCommand;
16
16
  pub enum MainPanelMsg {
17
17
  PointerEvent(web_sys::PointerEvent),
18
18
 
19
- /// The `Workspace` staged-panel set changed (`staged_changed` PubSub —
20
- /// see `Workspace::stage_panel`): re-render so cells/tabs/wrappers
19
+ /// A panel's `PanelPhase` changed (`staged_changed` PubSub — see
20
+ /// `Workspace::insert_panel`/`promote`): re-render so cells/tabs/wrappers
21
21
  /// reflect it and `reconcile` inserts a promoted panel. Subscribed on
22
22
  /// MainPanel's OWN scope — the root's `LayoutChanged` delivery proved
23
23
  /// unreliable from element-API task contexts (promoted panels stranded
@@ -41,7 +41,7 @@ const LAYOUT_PHYSICS: LayoutPhysics = LayoutPhysics {
41
41
 
42
42
  impl MainPanel {
43
43
  /// Size each STAGED panel's hidden wrapper (`.psp-staging` — see
44
- /// `MainPanel::render` and `Workspace::stage_panel`) to its PREDICTED
44
+ /// `MainPanel::render` and `PanelPhase::Staging`) to its PREDICTED
45
45
  /// post-insert cell: an equal share of the layout box's width (the
46
46
  /// reconcile insert splits the root horizontally with equal
47
47
  /// redistribution) minus the frame-chrome fallback the presize sweep
@@ -139,7 +139,7 @@ impl MainPanel {
139
139
  let layout: RegularLayout = el.unchecked_into();
140
140
  let panel_ids = &ctx.props().panel_ids;
141
141
 
142
- // Whole-element restore stages its saved layout tree on the Workspace
142
+ // `restoreWorkspace` stages its saved layout tree on the Workspace
143
143
  // (the model; regular-layout is a slave view). Apply it here, BEFORE
144
144
  // the insert reconcile, and seed `inserted` from its panel names — so
145
145
  // restored panels mount directly at their saved positions in ONE
@@ -165,8 +165,8 @@ impl MainPanel {
165
165
 
166
166
  // STAGED panels are withheld from the layout: their first draw
167
167
  // is completing in the hidden staging wrapper (see
168
- // `Workspace::stage_panel`). The promote re-render clears the
169
- // flag, and this loop then inserts the already-drawn panel.
168
+ // `PanelPhase::Staging`). The promote re-render flips the phase,
169
+ // and this loop then inserts the already-drawn panel.
170
170
  if ctx.props().workspace.is_staged(id) {
171
171
  continue;
172
172
  }