@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
@@ -17,20 +17,27 @@
17
17
  //! [`ColorSelector`]) so that they visually match the rich Yew widgets in
18
18
  //! the same sidebar.
19
19
 
20
+ use std::collections::BTreeMap;
20
21
  use std::rc::Rc;
21
22
 
22
23
  use itertools::Itertools;
23
24
  use serde_json::Value;
24
25
  use wasm_bindgen::JsCast;
25
26
  use web_sys::{HtmlInputElement, MouseEvent};
26
- use yew::{Callback, Html, Properties, classes, function_component, html, use_callback};
27
+ use yew::{Callback, Html, Properties, function_component, html, use_callback};
27
28
 
28
29
  use crate::components::containers::select::{Select, SelectItem};
29
- use crate::components::form::color_range_selector::ColorRangeSelector;
30
30
  use crate::components::form::color_selector::ColorSelector;
31
+ use crate::components::form::multi_stop_gradient_selector::MultiStopGradientSelector;
32
+ use crate::components::form::named_value_picker::NamedValuePicker;
31
33
  use crate::components::form::number_field::NumberField;
32
34
  use crate::components::form::optional_field::OptionalField;
33
- use crate::config::{ColumnConfigFieldUpdate, EnumVariant};
35
+ use crate::components::form::palette_selector::PaletteSelector;
36
+ use crate::config::{
37
+ ColumnConfigFieldUpdate, CssColor, CssGradient, CssKind, CssPalette, EnumVariant,
38
+ GradientStopSpec, NamedValue, canonicalize_css_color, discrete_pair, gradient_to_css,
39
+ palette_name_for,
40
+ };
34
41
 
35
42
  fn emit(on_change: &Callback<ColumnConfigFieldUpdate>, key: &str, value: Option<Value>) {
36
43
  let mut map = serde_json::Map::new();
@@ -44,28 +51,6 @@ fn emit(on_change: &Callback<ColumnConfigFieldUpdate>, key: &str, value: Option<
44
51
  });
45
52
  }
46
53
 
47
- fn emit_color_range(
48
- on_change: &Callback<ColumnConfigFieldUpdate>,
49
- key_pos: &str,
50
- key_neg: &str,
51
- default_pos: &str,
52
- default_neg: &str,
53
- new_pos: &str,
54
- new_neg: &str,
55
- ) {
56
- let mut value = serde_json::Map::new();
57
- if new_pos != default_pos {
58
- value.insert(key_pos.to_owned(), Value::String(new_pos.to_owned()));
59
- }
60
- if new_neg != default_neg {
61
- value.insert(key_neg.to_owned(), Value::String(new_neg.to_owned()));
62
- }
63
- on_change.emit(ColumnConfigFieldUpdate {
64
- keys: vec![key_pos.to_owned(), key_neg.to_owned()],
65
- value,
66
- });
67
- }
68
-
69
54
  #[derive(Properties, PartialEq)]
70
55
  pub struct EnumFieldProps {
71
56
  pub field_key: String,
@@ -239,106 +224,242 @@ pub fn NumberFieldPrimitive(props: &NumberFieldPrimitiveProps) -> Html {
239
224
  }
240
225
  }
241
226
 
227
+ #[allow(clippy::too_many_arguments)]
228
+ fn css_field_controls(
229
+ kind: CssKind,
230
+ named: &Rc<Vec<NamedValue>>,
231
+ restored: &BTreeMap<String, String>,
232
+ current: &str,
233
+ is_modified: bool,
234
+ on_select: Callback<String>,
235
+ on_pin: Callback<()>,
236
+ ) -> Html {
237
+ let can_pin = is_modified && palette_name_for(restored, kind, current).is_none();
238
+ if named.is_empty() && !can_pin {
239
+ return html! {};
240
+ }
241
+
242
+ html! { <NamedValuePicker {kind} entries={named.clone()} {on_select} {can_pin} {on_pin} /> }
243
+ }
244
+
245
+ fn named_literal(named: &[NamedValue], name: &str) -> Option<String> {
246
+ named
247
+ .iter()
248
+ .find(|entry| entry.name == name)
249
+ .map(|entry| entry.value.clone())
250
+ }
251
+
242
252
  #[derive(Properties, PartialEq)]
243
- pub struct ColorRangeFieldProps {
244
- pub field_key_pos: String,
245
- pub field_key_neg: String,
246
- pub default_pos: String,
247
- pub default_neg: String,
248
- pub current_pos: Option<String>,
249
- pub current_neg: Option<String>,
250
- pub is_gradient: bool,
253
+ pub struct PaletteFieldProps {
254
+ pub field_key: String,
255
+ /// Canonical palette string.
256
+ pub default: String,
257
+ pub max: Option<usize>,
258
+ /// Canonical palette string.
259
+ pub current: Option<String>,
251
260
  pub on_change: Callback<ColumnConfigFieldUpdate>,
261
+
262
+ /// Named palettes (workspace set ∪ theme), for the loader.
263
+ #[prop_or_default]
264
+ pub named: Rc<Vec<NamedValue>>,
265
+
266
+ /// The restored palette — the set Pin adds to.
267
+ #[prop_or_default]
268
+ pub restored: Rc<BTreeMap<String, String>>,
269
+
270
+ /// Pin the field's `(kind, literal)` into the restored palette.
271
+ #[prop_or_default]
272
+ pub on_pin: Callback<(CssKind, String)>,
273
+ }
274
+
275
+ fn palette_colors(src: &str, default: &str) -> Vec<String> {
276
+ let literal = |css: &str| match CssPalette::parse(css) {
277
+ Ok(CssPalette::Literal(colors)) => Some(colors),
278
+ _ => None,
279
+ };
280
+
281
+ literal(src)
282
+ .or_else(|| literal(default))
283
+ .unwrap_or_default()
252
284
  }
253
285
 
254
286
  #[function_component]
255
- pub fn ColorRangeField(props: &ColorRangeFieldProps) -> Html {
256
- let pos = props
257
- .current_pos
258
- .clone()
259
- .unwrap_or_else(|| props.default_pos.clone());
260
- let neg = props
261
- .current_neg
262
- .clone()
263
- .unwrap_or_else(|| props.default_neg.clone());
264
- let is_modified = (props.current_pos.is_some()
265
- && props.current_pos.as_deref() != Some(props.default_pos.as_str()))
266
- || (props.current_neg.is_some()
267
- && props.current_neg.as_deref() != Some(props.default_neg.as_str()));
268
-
269
- // Multi-key emit: write whichever side(s) differ from default,
270
- // clear the others. Mirrors the apply semantics of
271
- // `ColumnConfigFieldUpdate { keys, value }` with both keys owned.
272
- let on_pos_color = use_callback(
273
- (
274
- props.field_key_pos.clone(),
275
- props.field_key_neg.clone(),
276
- props.default_pos.clone(),
277
- props.default_neg.clone(),
278
- props.on_change.clone(),
279
- neg.clone(),
280
- ),
281
- |new_pos: String, (key_pos, key_neg, default_pos, default_neg, on_change, neg)| {
282
- emit_color_range(
283
- on_change,
284
- key_pos,
285
- key_neg,
286
- default_pos,
287
- default_neg,
288
- &new_pos,
289
- neg,
290
- );
291
- },
292
- );
287
+ pub fn PaletteField(props: &PaletteFieldProps) -> Html {
288
+ let current = props.current.as_deref().unwrap_or(&props.default);
289
+ let values = palette_colors(current, &props.default);
290
+ let is_modified = props.current.is_some() && props.current.as_ref() != Some(&props.default);
291
+ let emit_css = {
292
+ let key = props.field_key.clone();
293
+ let default = props.default.clone();
294
+ let on_change = props.on_change.clone();
295
+ move |css: String| {
296
+ if css == default {
297
+ emit(&on_change, &key, None);
298
+ } else {
299
+ emit(&on_change, &key, Some(Value::String(css)));
300
+ }
301
+ }
302
+ };
303
+
304
+ let on_change_palette = {
305
+ let emit_css = emit_css.clone();
306
+ Callback::from(move |values: Vec<String>| {
307
+ emit_css(CssPalette::Literal(values).to_css());
308
+ })
309
+ };
310
+
311
+ let on_select = {
312
+ let emit_css = emit_css.clone();
313
+ let named = props.named.clone();
314
+ Callback::from(move |name: String| {
315
+ if let Some(literal) = named_literal(&named, &name) {
316
+ emit_css(literal);
317
+ }
318
+ })
319
+ };
293
320
 
294
- let on_neg_color = use_callback(
295
- (
296
- props.field_key_pos.clone(),
297
- props.field_key_neg.clone(),
298
- props.default_pos.clone(),
299
- props.default_neg.clone(),
300
- props.on_change.clone(),
301
- pos.clone(),
302
- ),
303
- |new_neg: String, (key_pos, key_neg, default_pos, default_neg, on_change, pos)| {
304
- emit_color_range(
305
- on_change,
306
- key_pos,
307
- key_neg,
308
- default_pos,
309
- default_neg,
310
- pos,
311
- &new_neg,
312
- );
313
- },
321
+ let on_reset = use_callback(
322
+ (props.field_key.clone(), props.on_change.clone()),
323
+ |_: (), (key, on_change)| emit(on_change, key, None),
314
324
  );
315
325
 
326
+ let on_pin = {
327
+ let on_pin = props.on_pin.clone();
328
+ let literal = current.to_owned();
329
+ Callback::from(move |()| on_pin.emit((CssKind::Palette, literal.clone())))
330
+ };
331
+
332
+ html! {
333
+ <div class="row">
334
+ { css_field_controls(
335
+ CssKind::Palette,
336
+ &props.named,
337
+ &props.restored,
338
+ current,
339
+ is_modified,
340
+ on_select,
341
+ on_pin,
342
+ ) }
343
+ <PaletteSelector
344
+ {values}
345
+ max={props.max}
346
+ on_change={on_change_palette}
347
+ {on_reset}
348
+ {is_modified}
349
+ title={Some(format!("{}-label", props.field_key))}
350
+ />
351
+ </div>
352
+ }
353
+ }
354
+
355
+ #[derive(Properties, PartialEq)]
356
+ pub struct GradientStopsFieldProps {
357
+ pub field_key: String,
358
+ /// Canonical gradient string.
359
+ pub default: String,
360
+ pub discrete: bool,
361
+ /// Canonical gradient string.
362
+ pub current: Option<String>,
363
+ pub on_change: Callback<ColumnConfigFieldUpdate>,
364
+
365
+ /// Named gradients (workspace set ∪ theme), for the loader.
366
+ #[prop_or_default]
367
+ pub named: Rc<Vec<NamedValue>>,
368
+
369
+ /// The restored palette — the set Pin adds to.
370
+ #[prop_or_default]
371
+ pub restored: Rc<BTreeMap<String, String>>,
372
+
373
+ /// Pin the field's `(kind, literal)` into the restored palette.
374
+ #[prop_or_default]
375
+ pub on_pin: Callback<(CssKind, String)>,
376
+ }
377
+
378
+ fn gradient_stops(src: &str, default: &str) -> Vec<GradientStopSpec> {
379
+ let literal = |css: &str| match CssGradient::parse(css) {
380
+ Ok(CssGradient::Literal(stops)) => Some(stops),
381
+ _ => None,
382
+ };
383
+
384
+ literal(src)
385
+ .or_else(|| literal(default))
386
+ .unwrap_or_default()
387
+ }
388
+
389
+ #[function_component]
390
+ pub fn GradientStopsField(props: &GradientStopsFieldProps) -> Html {
391
+ let current = props.current.as_deref().unwrap_or(&props.default);
392
+ let stops = gradient_stops(current, &props.default);
393
+ let is_modified = props.current.is_some() && props.current.as_ref() != Some(&props.default);
394
+ let emit_css = {
395
+ let key = props.field_key.clone();
396
+ let default = props.default.clone();
397
+ let on_change = props.on_change.clone();
398
+ move |css: String| {
399
+ if css == default {
400
+ emit(&on_change, &key, None);
401
+ } else {
402
+ emit(&on_change, &key, Some(Value::String(css)));
403
+ }
404
+ }
405
+ };
406
+
407
+ let on_change_stops = {
408
+ let emit_css = emit_css.clone();
409
+ Callback::from(move |stops: Vec<GradientStopSpec>| {
410
+ emit_css(gradient_to_css(&stops));
411
+ })
412
+ };
413
+
414
+ let on_select = {
415
+ let emit_css = emit_css.clone();
416
+ let named = props.named.clone();
417
+ let discrete = props.discrete;
418
+ Callback::from(move |name: String| {
419
+ let Some(literal) = named_literal(&named, &name) else {
420
+ return;
421
+ };
422
+
423
+ let literal = match CssGradient::parse(&literal) {
424
+ Ok(CssGradient::Literal(stops)) if discrete && stops.len() > 2 => {
425
+ gradient_to_css(&discrete_pair(stops))
426
+ },
427
+ _ => literal,
428
+ };
429
+
430
+ emit_css(literal);
431
+ })
432
+ };
433
+
316
434
  let on_reset = use_callback(
317
- (
318
- props.field_key_pos.clone(),
319
- props.field_key_neg.clone(),
320
- props.on_change.clone(),
321
- ),
322
- |_: (), (key_pos, key_neg, on_change)| {
323
- on_change.emit(ColumnConfigFieldUpdate {
324
- keys: vec![key_pos.clone(), key_neg.clone()],
325
- value: serde_json::Map::new(),
326
- });
327
- },
435
+ (props.field_key.clone(), props.on_change.clone()),
436
+ |_: (), (key, on_change)| emit(on_change, key, None),
328
437
  );
329
438
 
439
+ let on_pin = {
440
+ let on_pin = props.on_pin.clone();
441
+ let literal = current.to_owned();
442
+ Callback::from(move |()| on_pin.emit((CssKind::Gradient, literal.clone())))
443
+ };
444
+
330
445
  html! {
331
446
  <div class="row">
332
- <ColorRangeSelector
333
- pos_class={classes!(props.field_key_pos.clone())}
334
- neg_class={classes!(props.field_key_neg.clone())}
335
- pos_color={pos}
336
- neg_color={neg}
337
- is_gradient={props.is_gradient}
338
- {on_pos_color}
339
- {on_neg_color}
447
+ { css_field_controls(
448
+ CssKind::Gradient,
449
+ &props.named,
450
+ &props.restored,
451
+ current,
452
+ is_modified,
453
+ on_select,
454
+ on_pin,
455
+ ) }
456
+ <MultiStopGradientSelector
457
+ {stops}
458
+ discrete={props.discrete}
459
+ on_change={on_change_stops}
340
460
  {on_reset}
341
461
  {is_modified}
462
+ title={Some(format!("{}-label", props.field_key))}
342
463
  />
343
464
  </div>
344
465
  }
@@ -347,42 +468,88 @@ pub fn ColorRangeField(props: &ColorRangeFieldProps) -> Html {
347
468
  #[derive(Properties, PartialEq)]
348
469
  pub struct ColorFieldProps {
349
470
  pub field_key: String,
471
+ /// Canonical `#rrggbb`.
350
472
  pub default: String,
473
+ /// Canonical `#rrggbb`.
351
474
  pub current: Option<String>,
352
475
  pub on_change: Callback<ColumnConfigFieldUpdate>,
476
+
477
+ /// Named colors (workspace set ∪ theme), for the loader.
478
+ #[prop_or_default]
479
+ pub named: Rc<Vec<NamedValue>>,
480
+
481
+ /// The restored palette — the set Pin adds to.
482
+ #[prop_or_default]
483
+ pub restored: Rc<BTreeMap<String, String>>,
484
+
485
+ /// Pin the field's `(kind, literal)` into the restored palette.
486
+ #[prop_or_default]
487
+ pub on_pin: Callback<(CssKind, String)>,
353
488
  }
354
489
 
355
490
  #[function_component]
356
491
  pub fn ColorField(props: &ColorFieldProps) -> Html {
357
- let color = props
358
- .current
359
- .clone()
360
- .unwrap_or_else(|| props.default.clone());
492
+ let current = props.current.as_deref().unwrap_or(&props.default);
493
+ let color = match CssColor::parse(current) {
494
+ Ok(CssColor::Literal(color)) => color,
495
+ _ => props.default.clone(),
496
+ };
497
+
361
498
  let is_modified =
362
499
  props.current.as_deref() != Some(props.default.as_str()) && props.current.is_some();
363
500
 
364
- let on_color = use_callback(
365
- (
366
- props.field_key.clone(),
367
- props.default.clone(),
368
- props.on_change.clone(),
369
- ),
370
- |value: String, (key, default, on_change)| {
371
- if value == *default {
372
- emit(on_change, key, None);
501
+ let emit_css = {
502
+ let key = props.field_key.clone();
503
+ let default = props.default.clone();
504
+ let on_change = props.on_change.clone();
505
+ move |css: String| {
506
+ if css == default {
507
+ emit(&on_change, &key, None);
373
508
  } else {
374
- emit(on_change, key, Some(Value::String(value)));
509
+ emit(&on_change, &key, Some(Value::String(css)));
375
510
  }
376
- },
377
- );
511
+ }
512
+ };
513
+
514
+ let on_color = {
515
+ let emit_css = emit_css.clone();
516
+ Callback::from(move |value: String| {
517
+ emit_css(canonicalize_css_color(&value).unwrap_or(value));
518
+ })
519
+ };
520
+
521
+ let on_select = {
522
+ let emit_css = emit_css.clone();
523
+ let named = props.named.clone();
524
+ Callback::from(move |name: String| {
525
+ if let Some(literal) = named_literal(&named, &name) {
526
+ emit_css(literal);
527
+ }
528
+ })
529
+ };
378
530
 
379
531
  let on_reset = use_callback(
380
532
  (props.field_key.clone(), props.on_change.clone()),
381
533
  |_: (), (key, on_change)| emit(on_change, key, None),
382
534
  );
383
535
 
536
+ let on_pin = {
537
+ let on_pin = props.on_pin.clone();
538
+ let literal = current.to_owned();
539
+ Callback::from(move |()| on_pin.emit((CssKind::Color, literal.clone())))
540
+ };
541
+
384
542
  html! {
385
543
  <div class="row">
544
+ { css_field_controls(
545
+ CssKind::Color,
546
+ &props.named,
547
+ &props.restored,
548
+ current,
549
+ is_modified,
550
+ on_select,
551
+ on_pin,
552
+ ) }
386
553
  <ColorSelector
387
554
  {color}
388
555
  {on_color}