@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
@@ -14,31 +14,35 @@ mod agg_depth_selector;
14
14
  pub(crate) mod primitive_field;
15
15
  mod symbol;
16
16
 
17
- use std::collections::HashMap;
17
+ use std::collections::{BTreeMap, HashMap};
18
+ use std::rc::Rc;
18
19
 
19
20
  use itertools::Itertools;
20
21
  use perspective_client::config::ColumnType;
21
- use yew::{Html, Properties, function_component, html};
22
+ use yew::{Callback, Html, Properties, function_component, html};
22
23
 
23
24
  use self::agg_depth_selector::*;
24
25
  use self::primitive_field::{
25
- BoolField, ColorField, ColorRangeField, EnumField, NumberFieldPrimitive,
26
+ BoolField, ColorField, EnumField, GradientStopsField, NumberFieldPrimitive, PaletteField,
26
27
  };
27
28
  use crate::components::column_settings_sidebar::style_tab::symbol::SymbolStyle;
29
+ use crate::components::containers::control_group::ControlGroup;
28
30
  use crate::components::datetime_column_style::DatetimeColumnStyle;
29
31
  use crate::components::number_series_style::NumberSeriesStyle;
30
32
  use crate::components::string_column_style::StringColumnStyle;
31
33
  use crate::components::style_controls::CustomNumberFormat;
32
34
  use crate::config::{
33
- ControlSpec, CustomNumberFormatConfig, DatetimeColumnStyleConfig, NumberSeriesStyleConfig,
35
+ ColumnConfigFieldUpdate, ControlSpec, CssKind, CustomNumberFormatConfig,
36
+ DatetimeColumnStyleConfig, NamedValue, NumberFormatDefaults, NumberSeriesStyleConfig,
34
37
  StringColumnStyleConfig,
35
38
  };
36
39
  use crate::presentation::Presentation;
37
- use crate::queries::{fetch_column_abs_max, get_column_config_schema};
40
+ use crate::queries::{fetch_column_abs_max, get_column_config_schema, named_values};
38
41
  use crate::renderer::Renderer;
39
42
  use crate::session::Session;
40
43
  use crate::tasks::send_column_config;
41
44
  use crate::utils::PtrEqRc;
45
+ use crate::workspace::Workspace;
42
46
 
43
47
  #[derive(Clone, PartialEq, Properties)]
44
48
  pub struct StyleTabProps {
@@ -62,6 +66,7 @@ pub struct StyleTabProps {
62
66
  pub presentation: Presentation,
63
67
  pub renderer: Renderer,
64
68
  pub session: Session,
69
+ pub workspace: Workspace,
65
70
  }
66
71
 
67
72
  #[function_component]
@@ -93,6 +98,33 @@ pub fn StyleTab(props: &StyleTabProps) -> Html {
93
98
  });
94
99
 
95
100
  let raw_config = props.renderer.get_columns_config(&props.column_name);
101
+
102
+ let named_for = |kind| Rc::new(named_values(&props.workspace, &props.presentation, kind));
103
+ let named_gradients = named_for(CssKind::Gradient);
104
+ let named_palettes = named_for(CssKind::Palette);
105
+ let named_colors = named_for(CssKind::Color);
106
+
107
+ let restored = Rc::new(props.presentation.palette());
108
+ let on_pin = {
109
+ let presentation = props.presentation.clone();
110
+ let revision = revision.clone();
111
+ yew::Callback::from(move |(kind, literal): (CssKind, String)| {
112
+ if let Err(error) = presentation.pin_style(kind, &literal) {
113
+ tracing::warn!("Failed to pin named {kind:?}: {error:?}");
114
+ }
115
+
116
+ revision.set(*revision + 1);
117
+ })
118
+ };
119
+ let schema = get_column_config_schema(
120
+ &props.renderer,
121
+ &props.view_config,
122
+ &props.metadata,
123
+ &props.column_name,
124
+ raw_config.as_ref(),
125
+ abs_max,
126
+ );
127
+
96
128
  let on_change = {
97
129
  let state = props.clone();
98
130
  let column_name = props.column_name.clone();
@@ -103,242 +135,306 @@ pub fn StyleTab(props: &StyleTabProps) -> Html {
103
135
  })
104
136
  };
105
137
 
106
- fn deser_sub<T: serde::de::DeserializeOwned>(
107
- raw: &Option<serde_json::Map<String, serde_json::Value>>,
108
- ) -> Option<T> {
109
- raw.as_ref()
110
- .and_then(|m| serde_json::from_value::<T>(serde_json::Value::Object(m.clone())).ok())
138
+ let on_group_toggle = {
139
+ let presentation = props.presentation.clone();
140
+ yew::Callback::from(move |(key, open): (String, bool)| {
141
+ presentation.set_control_group_collapsed(&key, !open);
142
+ })
143
+ };
144
+
145
+ let ctx = FieldRenderCtx {
146
+ props,
147
+ raw_config: &raw_config,
148
+ on_change: &on_change,
149
+ on_group_toggle: &on_group_toggle,
150
+ on_pin: &on_pin,
151
+ named_colors: &named_colors,
152
+ named_palettes: &named_palettes,
153
+ named_gradients: &named_gradients,
154
+ restored: &restored,
155
+ };
156
+
157
+ let components = schema
158
+ .map(|schema| render_specs(schema.fields, &ctx))
159
+ .unwrap_or_else(|error| {
160
+ tracing::error!("{}", error);
161
+ vec![]
162
+ });
163
+
164
+ html! {
165
+ <div id="style-tab">
166
+ <div id="column-style-container" class="tab-section">{ components }</div>
167
+ </div>
111
168
  }
169
+ }
112
170
 
113
- let components = get_column_config_schema(
114
- &props.renderer,
115
- &props.view_config,
116
- &props.metadata,
117
- &props.column_name,
118
- raw_config.as_ref(),
119
- abs_max,
120
- )
121
- .map(|schema| {
122
- schema
123
- .fields
124
- .into_iter()
125
- .filter_map(|spec| {
171
+ fn deser_sub<T: serde::de::DeserializeOwned>(
172
+ raw: &Option<serde_json::Map<String, serde_json::Value>>,
173
+ ) -> Option<T> {
174
+ raw.as_ref()
175
+ .and_then(|m| serde_json::from_value::<T>(serde_json::Value::Object(m.clone())).ok())
176
+ }
177
+
178
+ struct FieldRenderCtx<'a> {
179
+ props: &'a StyleTabProps,
180
+ raw_config: &'a Option<serde_json::Map<String, serde_json::Value>>,
181
+ on_change: &'a Callback<ColumnConfigFieldUpdate>,
182
+ on_group_toggle: &'a Callback<(String, bool)>,
183
+ on_pin: &'a Callback<(CssKind, String)>,
184
+ named_colors: &'a Rc<Vec<NamedValue>>,
185
+ named_palettes: &'a Rc<Vec<NamedValue>>,
186
+ named_gradients: &'a Rc<Vec<NamedValue>>,
187
+ restored: &'a Rc<BTreeMap<String, String>>,
188
+ }
189
+
190
+ fn render_specs(specs: Vec<ControlSpec>, ctx: &FieldRenderCtx) -> Vec<Html> {
191
+ specs
192
+ .into_iter()
193
+ .filter_map(|spec| match spec {
194
+ ControlSpec::Group { key, fields } => {
195
+ let children = render_specs(fields, ctx);
196
+ (!children.is_empty()).then(|| {
197
+ let open = !ctx.props.presentation.is_control_group_collapsed(&key);
198
+
199
+ html! {
200
+ <ControlGroup
201
+ key={format!("{}::group::{}", ctx.props.column_name, key)}
202
+ group_key={key.clone()}
203
+ {open}
204
+ on_toggle={ctx.on_group_toggle.clone()}
205
+ >
206
+ { children }
207
+ </ControlGroup>
208
+ }
209
+ })
210
+ },
211
+ spec => {
126
212
  let keys: Vec<String> = spec
127
213
  .serialized_keys()
128
214
  .into_iter()
129
215
  .map(|s| s.to_string())
130
216
  .collect();
131
- let component = match spec {
132
- ControlSpec::AggregateDepth => {
133
- let aggregate_depth = raw_config
134
- .as_ref()
135
- .and_then(|m| m.get("aggregate_depth"))
136
- .and_then(|v| v.as_u64())
137
- .unwrap_or(0) as u32;
138
- html! {
139
- <AggregateDepthSelector
140
- group_by_depth={props.group_by_depth}
141
- on_change={on_change.clone()}
142
- column_name={props.column_name.to_owned()}
143
- value={aggregate_depth}
144
- keys={keys.clone()}
145
- />
146
- }
147
- },
148
- ControlSpec::NumberSeriesStyle {
149
- default: default_config,
150
- } => {
151
- let config: Option<NumberSeriesStyleConfig> = deser_sub(&raw_config);
152
- html! {
153
- <NumberSeriesStyle
154
- {config}
155
- {default_config}
156
- on_change={on_change.clone()}
157
- keys={keys.clone()}
158
- />
159
- }
160
- },
161
- ControlSpec::DatetimeFormat => {
162
- let config: Option<DatetimeColumnStyleConfig> = deser_sub(&raw_config);
163
- let enable_time_config = props.ty.unwrap() == ColumnType::Datetime;
164
- html! {
165
- <DatetimeColumnStyle
166
- {enable_time_config}
167
- {config}
168
- on_change={on_change.clone()}
169
- keys={keys.clone()}
170
- />
171
- }
172
- },
173
- ControlSpec::StringFormat => {
174
- let config: Option<StringColumnStyleConfig> = deser_sub(&raw_config);
175
- html! {
176
- <StringColumnStyle
177
- {config}
178
- on_change={on_change.clone()}
179
- keys={keys.clone()}
180
- />
181
- }
182
- },
183
- ControlSpec::Symbols {
184
- default: default_config,
185
- } => {
186
- let restored_config: HashMap<String, String> = raw_config
187
- .as_ref()
188
- .and_then(|m| m.get("symbols"))
189
- .and_then(|v| serde_json::from_value(v.clone()).ok())
190
- .unwrap_or_default();
191
-
192
- html! {
193
- <SymbolStyle
194
- {default_config}
195
- restored_config={Some(restored_config)}
196
- on_change={on_change.clone()}
197
- column_name={props.column_name.clone()}
198
- selected_theme={props.selected_theme.clone()}
199
- session={props.session.clone()}
200
- keys={keys.clone()}
201
- />
202
- }
203
- },
204
- ControlSpec::NumberFormat => {
205
- let restored_config: CustomNumberFormatConfig = raw_config
206
- .as_ref()
207
- .and_then(|m| m.get("number_format"))
208
- .and_then(|v| serde_json::from_value(v.clone()).ok())
209
- .unwrap_or_default();
210
-
211
- html! {
212
- <CustomNumberFormat
213
- {restored_config}
214
- on_change={on_change.clone()}
215
- view_type={props.ty.unwrap()}
216
- column_name={props.column_name.clone()}
217
- keys={keys.clone()}
218
- />
219
- }
220
- },
221
- ControlSpec::Enum {
222
- key,
223
- variants,
224
- default,
225
- } => {
226
- let current = raw_config
227
- .as_ref()
228
- .and_then(|m| m.get(&key))
229
- .and_then(|v| v.as_str().map(|s| s.to_string()));
230
-
231
- html! {
232
- <EnumField
233
- field_key={key}
234
- {variants}
235
- {default}
236
- {current}
237
- on_change={on_change.clone()}
238
- />
239
- }
240
- },
241
- ControlSpec::Bool { key, default } => {
242
- let current = raw_config
243
- .as_ref()
244
- .and_then(|m| m.get(&key))
245
- .and_then(|v| v.as_bool());
246
- html! {
247
- <BoolField
248
- field_key={key}
249
- {default}
250
- {current}
251
- on_change={on_change.clone()}
252
- />
253
- }
254
- },
255
- ControlSpec::Color { key, default } => {
256
- let current = raw_config
257
- .as_ref()
258
- .and_then(|m| m.get(&key))
259
- .and_then(|v| v.as_str().map(|s| s.to_string()));
260
- html! {
261
- <ColorField
262
- field_key={key}
263
- {default}
264
- {current}
265
- on_change={on_change.clone()}
266
- />
267
- }
268
- },
269
- ControlSpec::ColorRange {
270
- key_pos,
271
- key_neg,
272
- default_pos,
273
- default_neg,
274
- is_gradient,
275
- } => {
276
- let current_pos = raw_config
277
- .as_ref()
278
- .and_then(|m| m.get(&key_pos))
279
- .and_then(|v| v.as_str().map(|s| s.to_string()));
280
- let current_neg = raw_config
281
- .as_ref()
282
- .and_then(|m| m.get(&key_neg))
283
- .and_then(|v| v.as_str().map(|s| s.to_string()));
284
- html! {
285
- <ColorRangeField
286
- field_key_pos={key_pos}
287
- field_key_neg={key_neg}
288
- {default_pos}
289
- {default_neg}
290
- {current_pos}
291
- {current_neg}
292
- {is_gradient}
293
- on_change={on_change.clone()}
294
- />
295
- }
296
- },
297
- ControlSpec::Number {
298
- key,
299
- default,
300
- min,
301
- max,
302
- step,
303
- include,
304
- } => {
305
- let current = raw_config
306
- .as_ref()
307
- .and_then(|m| m.get(&key))
308
- .and_then(|v| v.as_f64());
309
- html! {
310
- <NumberFieldPrimitive
311
- field_key={key}
312
- {default}
313
- {current}
314
- {min}
315
- {max}
316
- {step}
317
- {include}
318
- on_change={on_change.clone()}
319
- />
320
- }
321
- },
322
- // String primitive has no caller yet — wire when a
323
- // plugin emits one.
324
- ControlSpec::String { .. } => {
325
- return None;
326
- },
327
- };
328
-
329
- let key = format!("{}::{}", props.column_name, keys.join("+"));
217
+
218
+ let component = render_leaf(spec, &keys, ctx)?;
219
+ let key = format!("{}::{}", ctx.props.column_name, keys.join("+"));
330
220
  Some(html! { <fieldset class="style-control" {key}>{ component }</fieldset> })
331
- })
332
- .collect_vec()
333
- })
334
- .unwrap_or_else(|error| {
335
- tracing::error!("{}", error);
336
- vec![]
337
- });
221
+ },
222
+ })
223
+ .collect_vec()
224
+ }
338
225
 
339
- html! {
340
- <div id="style-tab">
341
- <div id="column-style-container" class="tab-section">{ components }</div>
342
- </div>
343
- }
226
+ fn render_leaf(spec: ControlSpec, keys: &[String], ctx: &FieldRenderCtx) -> Option<Html> {
227
+ let props = ctx.props;
228
+ let raw_config = ctx.raw_config;
229
+ let on_change = ctx.on_change;
230
+ let component = match spec {
231
+ ControlSpec::AggregateDepth => {
232
+ let aggregate_depth = raw_config
233
+ .as_ref()
234
+ .and_then(|m| m.get("aggregate_depth"))
235
+ .and_then(|v| v.as_u64())
236
+ .unwrap_or(0) as u32;
237
+ html! {
238
+ <AggregateDepthSelector
239
+ group_by_depth={props.group_by_depth}
240
+ on_change={on_change.clone()}
241
+ column_name={props.column_name.to_owned()}
242
+ value={aggregate_depth}
243
+ keys={keys.to_vec()}
244
+ />
245
+ }
246
+ },
247
+ ControlSpec::NumberSeriesStyle {
248
+ default: default_config,
249
+ } => {
250
+ let config: Option<NumberSeriesStyleConfig> = deser_sub(raw_config);
251
+ html! {
252
+ <NumberSeriesStyle
253
+ {config}
254
+ {default_config}
255
+ on_change={on_change.clone()}
256
+ keys={keys.to_vec()}
257
+ />
258
+ }
259
+ },
260
+ ControlSpec::DatetimeFormat { default } => {
261
+ let config: Option<DatetimeColumnStyleConfig> = deser_sub(raw_config);
262
+ let enable_time_config = props.ty.unwrap() == ColumnType::Datetime;
263
+ let default_format = Rc::new(default.unwrap_or_default());
264
+ html! {
265
+ <DatetimeColumnStyle
266
+ {enable_time_config}
267
+ {config}
268
+ {default_format}
269
+ on_change={on_change.clone()}
270
+ keys={keys.to_vec()}
271
+ />
272
+ }
273
+ },
274
+ ControlSpec::StringFormat => {
275
+ let config: Option<StringColumnStyleConfig> = deser_sub(raw_config);
276
+ html! {
277
+ <StringColumnStyle {config} on_change={on_change.clone()} keys={keys.to_vec()} />
278
+ }
279
+ },
280
+ ControlSpec::Symbols {
281
+ default: default_config,
282
+ } => {
283
+ let restored_config: HashMap<String, String> = raw_config
284
+ .as_ref()
285
+ .and_then(|m| m.get("symbols"))
286
+ .and_then(|v| serde_json::from_value(v.clone()).ok())
287
+ .unwrap_or_default();
288
+
289
+ html! {
290
+ <SymbolStyle
291
+ {default_config}
292
+ restored_config={Some(restored_config)}
293
+ on_change={on_change.clone()}
294
+ column_name={props.column_name.clone()}
295
+ selected_theme={props.selected_theme.clone()}
296
+ session={props.session.clone()}
297
+ keys={keys.to_vec()}
298
+ />
299
+ }
300
+ },
301
+ ControlSpec::NumberFormat { default } => {
302
+ let restored_config: CustomNumberFormatConfig = raw_config
303
+ .as_ref()
304
+ .and_then(|m| m.get("number_format"))
305
+ .and_then(|v| serde_json::from_value(v.clone()).ok())
306
+ .unwrap_or_default();
307
+
308
+ let view_type = props.ty.unwrap();
309
+ let defaults = Rc::new(NumberFormatDefaults::resolve(
310
+ default.as_ref(),
311
+ view_type == ColumnType::Float,
312
+ ));
313
+
314
+ html! {
315
+ <CustomNumberFormat
316
+ {restored_config}
317
+ {defaults}
318
+ on_change={on_change.clone()}
319
+ {view_type}
320
+ column_name={props.column_name.clone()}
321
+ keys={keys.to_vec()}
322
+ />
323
+ }
324
+ },
325
+ ControlSpec::Enum {
326
+ key,
327
+ variants,
328
+ default,
329
+ } => {
330
+ let current = raw_config
331
+ .as_ref()
332
+ .and_then(|m| m.get(&key))
333
+ .and_then(|v| v.as_str().map(|s| s.to_string()));
334
+
335
+ html! {
336
+ <EnumField
337
+ field_key={key}
338
+ {variants}
339
+ {default}
340
+ {current}
341
+ on_change={on_change.clone()}
342
+ />
343
+ }
344
+ },
345
+ ControlSpec::Bool { key, default } => {
346
+ let current = raw_config
347
+ .as_ref()
348
+ .and_then(|m| m.get(&key))
349
+ .and_then(|v| v.as_bool());
350
+ html! { <BoolField field_key={key} {default} {current} on_change={on_change.clone()} /> }
351
+ },
352
+ ControlSpec::Color { key, default } => {
353
+ let current = raw_config
354
+ .as_ref()
355
+ .and_then(|m| m.get(&key))
356
+ .and_then(|v| v.as_str().map(|s| s.to_string()));
357
+ html! {
358
+ <ColorField
359
+ field_key={key}
360
+ {default}
361
+ {current}
362
+ named={ctx.named_colors.clone()}
363
+ restored={ctx.restored.clone()}
364
+ on_pin={ctx.on_pin.clone()}
365
+ on_change={on_change.clone()}
366
+ />
367
+ }
368
+ },
369
+ ControlSpec::Palette { key, default, max } => {
370
+ let current = raw_config
371
+ .as_ref()
372
+ .and_then(|m| m.get(&key))
373
+ .and_then(|v| v.as_str().map(|s| s.to_string()));
374
+ html! {
375
+ <PaletteField
376
+ field_key={key}
377
+ {default}
378
+ {max}
379
+ {current}
380
+ named={ctx.named_palettes.clone()}
381
+ restored={ctx.restored.clone()}
382
+ on_pin={ctx.on_pin.clone()}
383
+ on_change={on_change.clone()}
384
+ />
385
+ }
386
+ },
387
+ ControlSpec::GradientStops {
388
+ key,
389
+ default,
390
+ discrete,
391
+ } => {
392
+ let current = raw_config
393
+ .as_ref()
394
+ .and_then(|m| m.get(&key))
395
+ .and_then(|v| v.as_str().map(|s| s.to_string()));
396
+ html! {
397
+ <GradientStopsField
398
+ field_key={key}
399
+ {default}
400
+ {discrete}
401
+ {current}
402
+ named={ctx.named_gradients.clone()}
403
+ restored={ctx.restored.clone()}
404
+ on_pin={ctx.on_pin.clone()}
405
+ on_change={on_change.clone()}
406
+ />
407
+ }
408
+ },
409
+ ControlSpec::Number {
410
+ key,
411
+ default,
412
+ min,
413
+ max,
414
+ step,
415
+ include,
416
+ } => {
417
+ let current = raw_config
418
+ .as_ref()
419
+ .and_then(|m| m.get(&key))
420
+ .and_then(|v| v.as_f64());
421
+ html! {
422
+ <NumberFieldPrimitive
423
+ field_key={key}
424
+ {default}
425
+ {current}
426
+ {min}
427
+ {max}
428
+ {step}
429
+ {include}
430
+ on_change={on_change.clone()}
431
+ />
432
+ }
433
+ },
434
+ ControlSpec::String { .. } | ControlSpec::Group { .. } => {
435
+ return None;
436
+ },
437
+ };
438
+
439
+ Some(component)
344
440
  }