@perspective-dev/viewer 5.3.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 (100) 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.inline.js +9 -9
  31. package/dist/esm/perspective-viewer.inline.js.map +3 -3
  32. package/dist/esm/perspective-viewer.js +8 -8
  33. package/dist/esm/perspective-viewer.js.map +3 -3
  34. package/dist/wasm/perspective-viewer.d.ts +16 -16
  35. package/dist/wasm/perspective-viewer.js +107 -103
  36. package/dist/wasm/perspective-viewer.wasm +0 -0
  37. package/dist/wasm/perspective-viewer.wasm.d.ts +16 -16
  38. package/package.json +1 -1
  39. package/src/css/column-settings-panel.css +19 -0
  40. package/src/css/column-style.css +82 -0
  41. package/src/css/containers/tabs.css +13 -3
  42. package/src/css/dom/select.css +19 -8
  43. package/src/css/plugin-selector.css +1 -0
  44. package/src/css/plugin-settings-panel.css +72 -0
  45. package/src/css/viewer.css +0 -5
  46. package/src/rust/agent/tools.rs +5 -2
  47. package/src/rust/components/column_settings_sidebar/style_tab.rs +296 -237
  48. package/src/rust/components/containers/control_group.rs +91 -0
  49. package/src/rust/components/containers/mod.rs +1 -0
  50. package/src/rust/components/containers/select.rs +8 -2
  51. package/src/rust/components/datetime_column_style/simple.rs +15 -4
  52. package/src/rust/components/datetime_column_style.rs +50 -10
  53. package/src/rust/components/plugin_tab.rs +125 -87
  54. package/src/rust/components/settings_panel.rs +1 -0
  55. package/src/rust/components/status_bar.rs +0 -1
  56. package/src/rust/components/style_controls/number_string_format/digits_section.rs +22 -10
  57. package/src/rust/components/style_controls/number_string_format/misc_section.rs +6 -2
  58. package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -0
  59. package/src/rust/components/style_controls/number_string_format/types.rs +24 -0
  60. package/src/rust/components/style_controls/number_string_format.rs +37 -14
  61. package/src/rust/config/column_config_schema.rs +358 -27
  62. package/src/rust/config/datetime_column_style.rs +4 -21
  63. package/src/rust/config/number_string_format.rs +211 -18
  64. package/src/rust/presentation.rs +17 -0
  65. package/src/rust/queries/plugin_column_styles.rs +2 -2
  66. package/src/rust/renderer/dispatch.rs +34 -8
  67. package/src/rust/renderer/plugin_config.rs +55 -8
  68. package/src/themes/blueprint.css +190 -0
  69. package/src/themes/botanical.css +4 -17
  70. package/src/themes/defaults.css +2 -120
  71. package/src/themes/dracula.css +2 -14
  72. package/src/themes/eggplant.css +189 -0
  73. package/src/themes/gruvbox-dark.css +3 -15
  74. package/src/themes/gruvbox.css +1 -1
  75. package/src/themes/icons.css +1 -2
  76. package/src/themes/intl/de.css +23 -1
  77. package/src/themes/intl/es.css +23 -1
  78. package/src/themes/intl/fr.css +23 -1
  79. package/src/themes/intl/ja.css +23 -1
  80. package/src/themes/intl/pt.css +23 -1
  81. package/src/themes/intl/zh.css +23 -1
  82. package/src/themes/intl.css +26 -4
  83. package/src/themes/ledger.css +195 -0
  84. package/src/themes/monokai.css +2 -14
  85. package/src/themes/nord.css +186 -0
  86. package/src/themes/phosphor.css +2 -14
  87. package/src/themes/pro-dark.css +2 -16
  88. package/src/themes/pro.css +118 -0
  89. package/src/themes/solarized-dark.css +2 -16
  90. package/src/themes/themes.css +10 -5
  91. package/src/themes/vaporwave.css +2 -14
  92. package/src/themes/velvet.css +192 -0
  93. package/src/ts/column-format.ts +52 -2
  94. package/src/svg/dropdown-selector-light.svg +0 -3
  95. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline0.js +0 -0
  96. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline1.js +0 -0
  97. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline2.js +0 -0
  98. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline3.js +0 -0
  99. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline4.js +0 -0
  100. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline5.js +0 -0
@@ -14,25 +14,27 @@ 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
18
  use std::rc::Rc;
19
19
 
20
20
  use itertools::Itertools;
21
21
  use perspective_client::config::ColumnType;
22
- use yew::{Html, Properties, function_component, html};
22
+ use yew::{Callback, Html, Properties, function_component, html};
23
23
 
24
24
  use self::agg_depth_selector::*;
25
25
  use self::primitive_field::{
26
26
  BoolField, ColorField, EnumField, GradientStopsField, NumberFieldPrimitive, PaletteField,
27
27
  };
28
28
  use crate::components::column_settings_sidebar::style_tab::symbol::SymbolStyle;
29
+ use crate::components::containers::control_group::ControlGroup;
29
30
  use crate::components::datetime_column_style::DatetimeColumnStyle;
30
31
  use crate::components::number_series_style::NumberSeriesStyle;
31
32
  use crate::components::string_column_style::StringColumnStyle;
32
33
  use crate::components::style_controls::CustomNumberFormat;
33
34
  use crate::config::{
34
- ControlSpec, CssKind, CustomNumberFormatConfig, DatetimeColumnStyleConfig,
35
- NumberSeriesStyleConfig, StringColumnStyleConfig,
35
+ ColumnConfigFieldUpdate, ControlSpec, CssKind, CustomNumberFormatConfig,
36
+ DatetimeColumnStyleConfig, NamedValue, NumberFormatDefaults, NumberSeriesStyleConfig,
37
+ StringColumnStyleConfig,
36
38
  };
37
39
  use crate::presentation::Presentation;
38
40
  use crate::queries::{fetch_column_abs_max, get_column_config_schema, named_values};
@@ -133,241 +135,27 @@ pub fn StyleTab(props: &StyleTabProps) -> Html {
133
135
  })
134
136
  };
135
137
 
136
- fn deser_sub<T: serde::de::DeserializeOwned>(
137
- raw: &Option<serde_json::Map<String, serde_json::Value>>,
138
- ) -> Option<T> {
139
- raw.as_ref()
140
- .and_then(|m| serde_json::from_value::<T>(serde_json::Value::Object(m.clone())).ok())
141
- }
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
+ };
142
156
 
143
157
  let components = schema
144
- .map(|schema| {
145
- schema
146
- .fields
147
- .into_iter()
148
- .filter_map(|spec| {
149
- let keys: Vec<String> = spec
150
- .serialized_keys()
151
- .into_iter()
152
- .map(|s| s.to_string())
153
- .collect();
154
- let component = match spec {
155
- ControlSpec::AggregateDepth => {
156
- let aggregate_depth = raw_config
157
- .as_ref()
158
- .and_then(|m| m.get("aggregate_depth"))
159
- .and_then(|v| v.as_u64())
160
- .unwrap_or(0)
161
- as u32;
162
- html! {
163
- <AggregateDepthSelector
164
- group_by_depth={props.group_by_depth}
165
- on_change={on_change.clone()}
166
- column_name={props.column_name.to_owned()}
167
- value={aggregate_depth}
168
- keys={keys.clone()}
169
- />
170
- }
171
- },
172
- ControlSpec::NumberSeriesStyle {
173
- default: default_config,
174
- } => {
175
- let config: Option<NumberSeriesStyleConfig> = deser_sub(&raw_config);
176
- html! {
177
- <NumberSeriesStyle
178
- {config}
179
- {default_config}
180
- on_change={on_change.clone()}
181
- keys={keys.clone()}
182
- />
183
- }
184
- },
185
- ControlSpec::DatetimeFormat => {
186
- let config: Option<DatetimeColumnStyleConfig> = deser_sub(&raw_config);
187
- let enable_time_config = props.ty.unwrap() == ColumnType::Datetime;
188
- html! {
189
- <DatetimeColumnStyle
190
- {enable_time_config}
191
- {config}
192
- on_change={on_change.clone()}
193
- keys={keys.clone()}
194
- />
195
- }
196
- },
197
- ControlSpec::StringFormat => {
198
- let config: Option<StringColumnStyleConfig> = deser_sub(&raw_config);
199
- html! {
200
- <StringColumnStyle
201
- {config}
202
- on_change={on_change.clone()}
203
- keys={keys.clone()}
204
- />
205
- }
206
- },
207
- ControlSpec::Symbols {
208
- default: default_config,
209
- } => {
210
- let restored_config: HashMap<String, String> = raw_config
211
- .as_ref()
212
- .and_then(|m| m.get("symbols"))
213
- .and_then(|v| serde_json::from_value(v.clone()).ok())
214
- .unwrap_or_default();
215
-
216
- html! {
217
- <SymbolStyle
218
- {default_config}
219
- restored_config={Some(restored_config)}
220
- on_change={on_change.clone()}
221
- column_name={props.column_name.clone()}
222
- selected_theme={props.selected_theme.clone()}
223
- session={props.session.clone()}
224
- keys={keys.clone()}
225
- />
226
- }
227
- },
228
- ControlSpec::NumberFormat => {
229
- let restored_config: CustomNumberFormatConfig = raw_config
230
- .as_ref()
231
- .and_then(|m| m.get("number_format"))
232
- .and_then(|v| serde_json::from_value(v.clone()).ok())
233
- .unwrap_or_default();
234
-
235
- html! {
236
- <CustomNumberFormat
237
- {restored_config}
238
- on_change={on_change.clone()}
239
- view_type={props.ty.unwrap()}
240
- column_name={props.column_name.clone()}
241
- keys={keys.clone()}
242
- />
243
- }
244
- },
245
- ControlSpec::Enum {
246
- key,
247
- variants,
248
- default,
249
- } => {
250
- let current = raw_config
251
- .as_ref()
252
- .and_then(|m| m.get(&key))
253
- .and_then(|v| v.as_str().map(|s| s.to_string()));
254
-
255
- html! {
256
- <EnumField
257
- field_key={key}
258
- {variants}
259
- {default}
260
- {current}
261
- on_change={on_change.clone()}
262
- />
263
- }
264
- },
265
- ControlSpec::Bool { key, default } => {
266
- let current = raw_config
267
- .as_ref()
268
- .and_then(|m| m.get(&key))
269
- .and_then(|v| v.as_bool());
270
- html! {
271
- <BoolField
272
- field_key={key}
273
- {default}
274
- {current}
275
- on_change={on_change.clone()}
276
- />
277
- }
278
- },
279
- ControlSpec::Color { key, default } => {
280
- let current = raw_config
281
- .as_ref()
282
- .and_then(|m| m.get(&key))
283
- .and_then(|v| v.as_str().map(|s| s.to_string()));
284
- html! {
285
- <ColorField
286
- field_key={key}
287
- {default}
288
- {current}
289
- named={named_colors.clone()}
290
- restored={restored.clone()}
291
- on_pin={on_pin.clone()}
292
- on_change={on_change.clone()}
293
- />
294
- }
295
- },
296
- ControlSpec::Palette { key, default, max } => {
297
- let current = raw_config
298
- .as_ref()
299
- .and_then(|m| m.get(&key))
300
- .and_then(|v| v.as_str().map(|s| s.to_string()));
301
- html! {
302
- <PaletteField
303
- field_key={key}
304
- {default}
305
- {max}
306
- {current}
307
- named={named_palettes.clone()}
308
- restored={restored.clone()}
309
- on_pin={on_pin.clone()}
310
- on_change={on_change.clone()}
311
- />
312
- }
313
- },
314
- ControlSpec::GradientStops {
315
- key,
316
- default,
317
- discrete,
318
- } => {
319
- let current = raw_config
320
- .as_ref()
321
- .and_then(|m| m.get(&key))
322
- .and_then(|v| v.as_str().map(|s| s.to_string()));
323
- html! {
324
- <GradientStopsField
325
- field_key={key}
326
- {default}
327
- {discrete}
328
- {current}
329
- named={named_gradients.clone()}
330
- restored={restored.clone()}
331
- on_pin={on_pin.clone()}
332
- on_change={on_change.clone()}
333
- />
334
- }
335
- },
336
- ControlSpec::Number {
337
- key,
338
- default,
339
- min,
340
- max,
341
- step,
342
- include,
343
- } => {
344
- let current = raw_config
345
- .as_ref()
346
- .and_then(|m| m.get(&key))
347
- .and_then(|v| v.as_f64());
348
- html! {
349
- <NumberFieldPrimitive
350
- field_key={key}
351
- {default}
352
- {current}
353
- {min}
354
- {max}
355
- {step}
356
- {include}
357
- on_change={on_change.clone()}
358
- />
359
- }
360
- },
361
- ControlSpec::String { .. } => {
362
- return None;
363
- },
364
- };
365
-
366
- let key = format!("{}::{}", props.column_name, keys.join("+"));
367
- Some(html! { <fieldset class="style-control" {key}>{ component }</fieldset> })
368
- })
369
- .collect_vec()
370
- })
158
+ .map(|schema| render_specs(schema.fields, &ctx))
371
159
  .unwrap_or_else(|error| {
372
160
  tracing::error!("{}", error);
373
161
  vec![]
@@ -379,3 +167,274 @@ pub fn StyleTab(props: &StyleTabProps) -> Html {
379
167
  </div>
380
168
  }
381
169
  }
170
+
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 => {
212
+ let keys: Vec<String> = spec
213
+ .serialized_keys()
214
+ .into_iter()
215
+ .map(|s| s.to_string())
216
+ .collect();
217
+
218
+ let component = render_leaf(spec, &keys, ctx)?;
219
+ let key = format!("{}::{}", ctx.props.column_name, keys.join("+"));
220
+ Some(html! { <fieldset class="style-control" {key}>{ component }</fieldset> })
221
+ },
222
+ })
223
+ .collect_vec()
224
+ }
225
+
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)
440
+ }
@@ -0,0 +1,91 @@
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::{Event, MouseEvent};
15
+ use yew::{Callback, Html, Properties, function_component, html, use_callback, use_mut_ref};
16
+
17
+ #[derive(Properties, PartialEq)]
18
+ pub struct ControlGroupProps {
19
+ pub group_key: String,
20
+
21
+ pub open: bool,
22
+
23
+ pub on_toggle: Callback<(String, bool)>,
24
+
25
+ pub children: Html,
26
+ }
27
+
28
+ #[function_component]
29
+ pub fn ControlGroup(props: &ControlGroupProps) -> Html {
30
+ let ontoggle = use_callback(
31
+ (props.group_key.clone(), props.on_toggle.clone()),
32
+ |event: Event, (group_key, on_toggle)| {
33
+ if let Some(el) = event
34
+ .target()
35
+ .and_then(|x| x.dyn_into::<web_sys::Element>().ok())
36
+ {
37
+ on_toggle.emit((group_key.clone(), el.has_attribute("open")));
38
+ }
39
+ },
40
+ );
41
+
42
+ let onclick = use_callback((), |event: MouseEvent, _| {
43
+ if !event.shift_key() {
44
+ return;
45
+ }
46
+
47
+ event.prevent_default();
48
+ let Some(details) = event
49
+ .target()
50
+ .and_then(|x| x.dyn_into::<web_sys::Element>().ok())
51
+ .and_then(|x| x.closest("details.control-group").ok().flatten())
52
+ else {
53
+ return;
54
+ };
55
+
56
+ let Some(scope) = details.closest(".tab-section").ok().flatten() else {
57
+ return;
58
+ };
59
+
60
+ let expand = !details.has_attribute("open");
61
+ let Ok(groups) = scope.query_selector_all("details.control-group") else {
62
+ return;
63
+ };
64
+
65
+ for i in 0..groups.length() {
66
+ let Some(el) = groups
67
+ .item(i)
68
+ .and_then(|x| x.dyn_into::<web_sys::Element>().ok())
69
+ else {
70
+ continue;
71
+ };
72
+
73
+ if expand {
74
+ let _ = el.set_attribute("open", "");
75
+ } else {
76
+ let _ = el.remove_attribute("open");
77
+ }
78
+ }
79
+ });
80
+
81
+ let open = *use_mut_ref(|| props.open).borrow();
82
+ html! {
83
+ <details class="control-group" {open} {ontoggle}>
84
+ <summary {onclick}>
85
+ <span class="control-group-chevron shift-alt-icon" />
86
+ <label id={format!("{}-group-label", props.group_key)} />
87
+ </summary>
88
+ { props.children.clone() }
89
+ </details>
90
+ }
91
+ }
@@ -13,6 +13,7 @@
13
13
  //! `containers` are generic container-like components for collections of other
14
14
  //! `Component` types.
15
15
 
16
+ pub mod control_group;
16
17
  pub mod dragdrop_list;
17
18
  pub mod dropdown_menu;
18
19
  pub mod scroll_panel;
@@ -230,9 +230,15 @@ where
230
230
  <label>
231
231
  { ctx.props().label.as_ref().map(|x| x.to_string()).unwrap_or_default() }
232
232
  </label>
233
- <div class={wrapper_class} data-value={value}>{ select }</div>
233
+ <div class={wrapper_class} data-value={value}>
234
+ { select }
235
+ <span class="select-arrow" />
236
+ </div>
234
237
  } else {
235
- <div class={wrapper_class} data-value={value}>{ select }</div>
238
+ <div class={wrapper_class} data-value={value}>
239
+ { select }
240
+ <span class="select-arrow" />
241
+ </div>
236
242
  }
237
243
  }
238
244
  }