@perspective-dev/viewer 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/dist/cdn/perspective-viewer.js +9 -9
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/docs/perspective-docs.json +1 -0
  24. package/dist/esm/column-format.d.ts +19 -28
  25. package/dist/esm/perspective-viewer.d.ts +2 -0
  26. package/dist/esm/perspective-viewer.inline.js +8 -8
  27. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  28. package/dist/esm/perspective-viewer.js +9 -9
  29. package/dist/esm/perspective-viewer.js.map +4 -4
  30. package/dist/esm/providers.d.ts +79 -0
  31. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  32. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  33. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  34. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  35. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  36. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  37. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  38. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  39. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  40. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  41. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  42. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  43. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  44. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  45. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +43 -1
  46. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  47. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  48. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  49. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  50. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  51. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  52. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  53. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  54. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  55. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  56. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  57. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  58. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  59. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  60. package/dist/esm/ts-rs/ViewerConfig.d.ts +43 -1
  61. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  62. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +72 -0
  63. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  64. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  65. package/dist/esm/ts-rs/WindowSpec.d.ts +47 -0
  66. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  67. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
  68. package/dist/wasm/perspective-viewer.d.ts +222 -26
  69. package/dist/wasm/perspective-viewer.js +378 -125
  70. package/dist/wasm/perspective-viewer.wasm +0 -0
  71. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
  72. package/package.json +2 -1
  73. package/src/css/_viewer_bundle.css +1 -0
  74. package/src/css/column-selector.css +9 -0
  75. package/src/css/column-settings-panel.css +67 -2
  76. package/src/css/column-style.css +4 -3
  77. package/src/css/config-selector.css +60 -4
  78. package/src/css/containers/context-menu.css +12 -6
  79. package/src/css/containers/tabs.css +2 -2
  80. package/src/css/form/code-editor.css +2 -3
  81. package/src/css/form/mirrored-textarea.css +81 -0
  82. package/src/css/viewer.css +336 -0
  83. package/src/rust/agent/client/mod.rs +19 -0
  84. package/src/rust/agent/client/protocol.rs +320 -0
  85. package/src/rust/agent/client/transport.rs +287 -0
  86. package/src/rust/agent/client/turn.rs +174 -0
  87. package/src/rust/agent/config.rs +159 -0
  88. package/src/rust/agent/docs.rs +293 -0
  89. package/src/rust/agent/markdown/block.rs +509 -0
  90. package/src/rust/agent/markdown/entity.rs +52 -0
  91. package/src/rust/agent/markdown/events.rs +71 -0
  92. package/src/rust/agent/markdown/inline.rs +497 -0
  93. package/src/rust/agent/markdown/mod.rs +466 -0
  94. package/src/rust/agent/mod.rs +23 -0
  95. package/src/rust/agent/preamble.md +16 -0
  96. package/src/rust/agent/runtime.rs +201 -0
  97. package/src/rust/agent/slot.rs +236 -0
  98. package/src/rust/agent/tools.rs +1110 -0
  99. package/src/rust/components/chat_panel.rs +289 -0
  100. package/src/rust/components/column_selector/active_column.rs +21 -27
  101. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  102. package/src/rust/components/column_selector/config_selector.rs +83 -8
  103. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  104. package/src/rust/components/column_selector/inactive_column.rs +8 -1
  105. package/src/rust/components/column_selector.rs +27 -3
  106. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  107. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  108. package/src/rust/components/column_settings_sidebar.rs +163 -16
  109. package/src/rust/components/containers/dragdrop_list.rs +110 -3
  110. package/src/rust/components/containers/sidebar.rs +99 -22
  111. package/src/rust/components/containers/split_panel.rs +6 -0
  112. package/src/rust/components/editable_header.rs +2 -0
  113. package/src/rust/components/form/code_editor.rs +14 -23
  114. package/src/rust/components/form/debug.rs +8 -5
  115. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  116. package/src/rust/components/form/mod.rs +1 -0
  117. package/src/rust/components/main_panel.rs +5 -0
  118. package/src/rust/components/mod.rs +3 -0
  119. package/src/rust/components/panel_menu.rs +7 -0
  120. package/src/rust/components/panel_tab.rs +5 -0
  121. package/src/rust/components/portal.rs +7 -0
  122. package/src/rust/components/settings_panel.rs +80 -2
  123. package/src/rust/components/viewer/msg.rs +2 -0
  124. package/src/rust/components/viewer/panels.rs +9 -14
  125. package/src/rust/components/viewer/render.rs +14 -0
  126. package/src/rust/components/viewer/settings.rs +33 -1
  127. package/src/rust/components/viewer.rs +2 -0
  128. package/src/rust/components/window_editor.rs +985 -0
  129. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  130. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  131. package/src/rust/config/number_string_format.rs +33 -0
  132. package/src/rust/config/options.rs +18 -0
  133. package/src/rust/config/plugin_static_config.rs +120 -5
  134. package/src/rust/config/viewer_config.rs +220 -1
  135. package/src/rust/config/workspace_config.rs +7 -6
  136. package/src/rust/custom_elements/viewer.rs +209 -18
  137. package/src/rust/custom_events.rs +29 -0
  138. package/src/rust/lib.rs +17 -1
  139. package/src/rust/presentation/column_locator.rs +16 -1
  140. package/src/rust/presentation.rs +8 -0
  141. package/src/rust/queries/columns_iter_set.rs +7 -1
  142. package/src/rust/queries/get_viewer_config.rs +5 -1
  143. package/src/rust/renderer/dispatch.rs +85 -58
  144. package/src/rust/renderer.rs +34 -1
  145. package/src/rust/session/column_defaults_update.rs +18 -0
  146. package/src/rust/session/drag_drop_update.rs +18 -0
  147. package/src/rust/session/metadata.rs +64 -9
  148. package/src/rust/session/replace_expression_update.rs +2 -0
  149. package/src/rust/session.rs +7 -5
  150. package/src/rust/tasks/create_panel.rs +18 -8
  151. package/src/rust/tasks/edit_window.rs +155 -0
  152. package/src/rust/tasks/mod.rs +2 -0
  153. package/src/rust/tasks/restore_panel.rs +34 -17
  154. package/src/rust/tasks/sync_update_panels.rs +7 -23
  155. package/src/rust/utils/browser/dragdrop.rs +12 -0
  156. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  157. package/src/rust/utils/debounce.rs +76 -27
  158. package/src/rust/workspace.rs +112 -20
  159. package/src/svg/pin-icon.svg +5 -0
  160. package/src/themes/icons.css +1 -0
  161. package/src/themes/intl/de.css +23 -0
  162. package/src/themes/intl/es.css +23 -0
  163. package/src/themes/intl/fr.css +23 -0
  164. package/src/themes/intl/ja.css +23 -0
  165. package/src/themes/intl/pt.css +23 -0
  166. package/src/themes/intl/zh.css +23 -0
  167. package/src/themes/intl.css +26 -1
  168. package/src/ts/column-format.ts +67 -46
  169. package/src/ts/perspective-viewer.ts +2 -0
  170. package/src/ts/providers.ts +96 -0
  171. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  172. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  173. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  174. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  175. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  176. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  177. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  178. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  179. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  180. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  181. package/src/ts/ts-rs/FormatMode.ts +3 -0
  182. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  183. package/src/ts/ts-rs/Notation.ts +11 -0
  184. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  185. package/src/ts/ts-rs/PanelViewerConfig.ts +48 -1
  186. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  187. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  188. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  189. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  190. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  191. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  192. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  193. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  194. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  195. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  196. package/src/ts/ts-rs/Unit.ts +3 -0
  197. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  198. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  199. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  200. package/src/ts/ts-rs/ViewerConfig.ts +48 -1
  201. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  202. package/src/ts/ts-rs/ViewerConfigUpdate.ts +81 -2
  203. package/src/ts/ts-rs/WindowSort.ts +9 -0
  204. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  205. package/src/ts/ts-rs/WindowSpec.ts +47 -0
  206. package/src/ts/ts-rs/Windows.ts +10 -0
  207. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
  208. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
  209. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
  210. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
  211. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
  212. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
  213. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
@@ -62,14 +62,22 @@ const fn numeric_default() -> u32 {
62
62
  0
63
63
  }
64
64
 
65
+ /// A datetime column's `date_format` in its `Custom` form: per-part
66
+ /// `Intl.DateTimeFormatOptions` overrides, discriminated from the `Simple`
67
+ /// preset form by the required `format: "custom"` key. Each part is a
68
+ /// [`CustomDatetimeFormat`] variant valid for that `Intl` option;
69
+ /// `"disabled"` omits the part from the formatted output entirely.
65
70
  #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, TS)]
66
71
  pub struct CustomDatetimeStyleConfig {
67
72
  format: FormatUnit,
68
73
 
74
+ /// An IANA time zone name (e.g. `"America/New_York"`); defaults to the
75
+ /// browser's local time zone.
69
76
  #[serde(default)]
70
77
  #[serde(rename = "timeZone", skip_serializing_if = "Option::is_none")]
71
78
  pub time_zone: Option<String>,
72
79
 
80
+ /// Sub-second digits to display, 0-3 (0 = none).
73
81
  #[serde(
74
82
  skip_serializing_if = "is_zero",
75
83
  rename = "fractionalSecondDigits",
@@ -77,27 +85,36 @@ pub struct CustomDatetimeStyleConfig {
77
85
  )]
78
86
  pub fractional_seconds: u32,
79
87
 
88
+ /// Defaults to `"numeric"`.
80
89
  #[serde(skip_serializing_if = "is_numeric", default = "second_default")]
81
90
  pub second: CustomDatetimeFormat,
82
91
 
92
+ /// Defaults to `"numeric"`.
83
93
  #[serde(skip_serializing_if = "is_numeric", default = "second_default")]
84
94
  pub minute: CustomDatetimeFormat,
85
95
 
96
+ /// Defaults to `"numeric"`.
86
97
  #[serde(skip_serializing_if = "is_numeric", default = "second_default")]
87
98
  pub hour: CustomDatetimeFormat,
88
99
 
100
+ /// Defaults to `"numeric"`.
89
101
  #[serde(skip_serializing_if = "is_numeric", default = "second_default")]
90
102
  pub day: CustomDatetimeFormat,
91
103
 
104
+ /// A name form (`"long"`/`"short"`/`"narrow"`); defaults to
105
+ /// `"disabled"` (weekday not shown).
92
106
  #[serde(skip_serializing_if = "is_disabled", default = "weekday_default")]
93
107
  pub weekday: CustomDatetimeFormat,
94
108
 
109
+ /// Numeric or name form; defaults to `"numeric"`.
95
110
  #[serde(skip_serializing_if = "is_numeric", default = "second_default")]
96
111
  pub month: CustomDatetimeFormat,
97
112
 
113
+ /// Defaults to `"2-digit"`.
98
114
  #[serde(skip_serializing_if = "is_two_digit", default = "year_default")]
99
115
  pub year: CustomDatetimeFormat,
100
116
 
117
+ /// 12-hour clock; defaults to `true`.
101
118
  #[serde(skip_serializing_if = "is_true", default = "hour12_default")]
102
119
  pub hour12: bool,
103
120
  }
@@ -23,12 +23,20 @@ const fn time_style_default() -> SimpleDatetimeFormat {
23
23
  SimpleDatetimeFormat::Medium
24
24
  }
25
25
 
26
+ /// A datetime column's `date_format` in its `Simple` preset form:
27
+ /// `Intl.DateTimeFormatOptions`' `dateStyle`/`timeStyle` presets. This is
28
+ /// the default form (no `format` key); setting `format: "custom"` selects
29
+ /// the per-part `CustomDatetimeStyleConfig` instead.
26
30
  #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, TS)]
27
31
  pub struct SimpleDatetimeStyleConfig {
32
+ /// An IANA time zone name (e.g. `"America/New_York"`); defaults to the
33
+ /// browser's local time zone.
28
34
  #[serde(default)]
29
35
  #[serde(rename = "timeZone", skip_serializing_if = "Option::is_none")]
30
36
  pub time_zone: Option<String>,
31
37
 
38
+ /// Date preset breadth, or `"disabled"` to omit the date entirely;
39
+ /// defaults to `"short"`.
32
40
  #[serde(
33
41
  default = "date_style_default",
34
42
  rename = "dateStyle",
@@ -36,6 +44,8 @@ pub struct SimpleDatetimeStyleConfig {
36
44
  )]
37
45
  pub date_style: SimpleDatetimeFormat,
38
46
 
47
+ /// Time preset breadth, or `"disabled"` to omit the time entirely;
48
+ /// defaults to `"medium"`.
39
49
  #[serde(
40
50
  default = "time_style_default",
41
51
  rename = "timeStyle",
@@ -16,6 +16,11 @@ use serde::{Deserialize, Serialize};
16
16
  use strum::{Display, EnumIter};
17
17
  use ts_rs::TS;
18
18
 
19
+ /// The `style` family of a numeric column's `number_format` — serialized
20
+ /// FLATTENED into [`CustomNumberFormatConfig`]'s object, discriminated by
21
+ /// the `style` key (`"decimal"` default, `"currency"` + `currency`/
22
+ /// `currencyDisplay`/`currencySign`, `"percent"`, `"unit"` + `unit`/
23
+ /// `unitDisplay`), mirroring `Intl.NumberFormat` options.
19
24
  #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, TS)]
20
25
  #[serde(rename_all = "camelCase", tag = "style")]
21
26
  pub enum NumberFormatStyle {
@@ -127,6 +132,11 @@ pub enum TrailingZeroDisplay {
127
132
  StripIfInteger,
128
133
  }
129
134
 
135
+ /// The `notation` family of a numeric column's `number_format` —
136
+ /// serialized FLATTENED into [`CustomNumberFormatConfig`]'s object,
137
+ /// discriminated by the `notation` key (`"standard"` default,
138
+ /// `"scientific"`, `"engineering"`, `"compact"` + `compactDisplay`),
139
+ /// mirroring `Intl.NumberFormat` options.
130
140
  #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, TS)]
131
141
  #[serde(rename_all = "camelCase", tag = "notation")]
132
142
  pub enum Notation {
@@ -169,6 +179,14 @@ pub enum SignDisplay {
169
179
  Never,
170
180
  }
171
181
 
182
+ /// A numeric column's `number_format` (`columns_config` value) —
183
+ /// `Intl.NumberFormat`-shaped options, written by the Style tab's number
184
+ /// format editor and read by `createNumberFormatter`. The `style` and
185
+ /// `notation` families ([`NumberFormatStyle`] / [`Notation`]) are serde-
186
+ /// FLATTENED into this object but `#[ts(skip)]`'d (ts-rs cannot flatten
187
+ /// `Option<enum>`) — the package's `NumberFormatConfig` re-composes the
188
+ /// full wire type as `CustomNumberFormatConfig & Partial<NumberFormatStyle>
189
+ /// & Partial<Notation>` (see `column-format.ts`).
172
190
  #[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone, TS)]
173
191
  #[serde(rename_all = "camelCase")]
174
192
  pub struct CustomNumberFormatConfig {
@@ -181,33 +199,42 @@ pub struct CustomNumberFormatConfig {
181
199
  // these min/max props can all be specified but it results in possible conflicts
182
200
  // may consider making them distinct options
183
201
  #[serde(skip_serializing_if = "Option::is_none")]
202
+ #[ts(optional, as = "Option<_>")]
184
203
  pub minimum_integer_digits: Option<f64>,
185
204
 
186
205
  #[serde(skip_serializing_if = "Option::is_none")]
206
+ #[ts(optional, as = "Option<_>")]
187
207
  pub minimum_fraction_digits: Option<f64>,
188
208
 
189
209
  #[serde(skip_serializing_if = "Option::is_none")]
210
+ #[ts(optional, as = "Option<_>")]
190
211
  pub maximum_fraction_digits: Option<f64>,
191
212
 
192
213
  #[serde(skip_serializing_if = "Option::is_none")]
214
+ #[ts(optional, as = "Option<_>")]
193
215
  pub minimum_significant_digits: Option<f64>,
194
216
 
195
217
  #[serde(skip_serializing_if = "Option::is_none")]
218
+ #[ts(optional, as = "Option<_>")]
196
219
  pub maximum_significant_digits: Option<f64>,
197
220
 
198
221
  #[serde(skip_serializing_if = "Option::is_none")]
222
+ #[ts(optional, as = "Option<_>")]
199
223
  pub rounding_priority: Option<RoundingPriority>,
200
224
 
201
225
  // specific values https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingincrement
202
226
  // Only available with automatic rounding priority
203
227
  // Cannot be mixed with sigfig rounding. (Does this mean max/min sigfig must be unset?)
204
228
  #[serde(skip_serializing_if = "Option::is_none")]
229
+ #[ts(optional, as = "Option<_>")]
205
230
  pub rounding_increment: Option<f64>,
206
231
 
207
232
  #[serde(skip_serializing_if = "Option::is_none")]
233
+ #[ts(optional, as = "Option<_>")]
208
234
  pub rounding_mode: Option<RoundingMode>,
209
235
 
210
236
  #[serde(skip_serializing_if = "Option::is_none")]
237
+ #[ts(optional, as = "Option<_>")]
211
238
  pub trailing_zero_display: Option<TrailingZeroDisplay>,
212
239
 
213
240
  #[serde(flatten)]
@@ -215,10 +242,16 @@ pub struct CustomNumberFormatConfig {
215
242
  #[serde(skip_serializing_if = "Option::is_none")]
216
243
  pub _notation: Option<Notation>,
217
244
 
245
+ /// NOTE (audit 2026-08-05): serialized values are the STRINGS
246
+ /// `"always"`/`"auto"`/`"min2"` or the untagged BOOLEAN `false` —
247
+ /// the former hand-written TS `useGrouping?: boolean` was wrong for
248
+ /// the string cases.
218
249
  #[serde(skip_serializing_if = "Option::is_none")]
250
+ #[ts(optional, as = "Option<_>")]
219
251
  pub use_grouping: Option<UseGrouping>,
220
252
 
221
253
  #[serde(skip_serializing_if = "Option::is_none")]
254
+ #[ts(optional, as = "Option<_>")]
222
255
  pub sign_display: Option<SignDisplay>,
223
256
  }
224
257
 
@@ -28,6 +28,24 @@ pub struct PanelOptions {
28
28
  pub panel: Option<String>,
29
29
  }
30
30
 
31
+ /// Options for the `restore()` method.
32
+ #[derive(Deserialize, Default, TS)]
33
+ pub struct RestoreOptions {
34
+ /// The target panel; the active panel when omitted.
35
+ #[ts(optional)]
36
+ pub panel: Option<String>,
37
+
38
+ /// When `true`, a failed restore only REJECTS the returned `Promise` —
39
+ /// the error is not committed to the viewer's visible error state, and
40
+ /// the session remains usable for subsequent calls. For programmatic
41
+ /// callers (e.g. the LLM agent's `set_view_config` tool) for whom a
42
+ /// failed config patch is feedback rather than a user-facing fault.
43
+ /// The config may be partially applied on failure; restore a
44
+ /// known-good config to recover exactly.
45
+ #[ts(optional)]
46
+ pub suppress_errors: Option<bool>,
47
+ }
48
+
31
49
  /// The `eject` argument: the loaded client to remove by name; the active
32
50
  /// panel's client when omitted.
33
51
  #[derive(Deserialize, Default, TS)]
@@ -10,7 +10,7 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use perspective_client::config::GroupRollupMode;
13
+ use perspective_client::config::{GroupRollupMode, SplitRollupMode};
14
14
  use serde::Deserialize;
15
15
  use ts_rs::TS;
16
16
 
@@ -103,6 +103,13 @@ pub struct PluginStaticConfig {
103
103
  #[ts(optional)]
104
104
  pub group_rollup_modes: Option<Vec<GroupRollupMode>>,
105
105
 
106
+ /// Split-rollup modes the plugin accepts, in preference order.
107
+ /// The first entry that matches a feature flag becomes the default.
108
+ #[serde(default)]
109
+ #[ts(as = "Option<_>")]
110
+ #[ts(optional)]
111
+ pub split_rollup_modes: Option<Vec<SplitRollupMode>>,
112
+
106
113
  /// Plugin load priority. Higher numbers win; ties resolve in
107
114
  /// registration order. The highest-priority plugin is loaded by
108
115
  /// default unless `restore({ plugin })` overrides it.
@@ -120,15 +127,70 @@ pub struct PluginStaticConfig {
120
127
  #[ts(as = "Option<_>")]
121
128
  #[ts(optional)]
122
129
  pub can_render_column_styles: bool,
130
+
131
+ /// What `group_by` MEANS visually for this plugin, e.g. `"X Axis"`
132
+ /// for the Y-series charts or `"Hierarchy"` for treemap/sunburst.
133
+ /// `None` where the field has no visual role of its own and is a
134
+ /// plain aggregation key (the X/Y charts, whose axes both come from
135
+ /// `columns`).
136
+ ///
137
+ /// This is the `group_by` counterpart of [`Self::config_column_names`]:
138
+ /// the same declaration that names the positional `columns` slots
139
+ /// should say what the other view fields draw, so that consumers —
140
+ /// the settings UI's field labels, and the agent's `list_plugins`
141
+ /// contract — read one source instead of restating the mapping.
142
+ #[serde(default)]
143
+ #[ts(as = "Option<_>")]
144
+ #[ts(optional)]
145
+ pub group_by_role: Option<String>,
146
+
147
+ /// What `split_by` MEANS visually for this plugin, e.g. `"Series"`.
148
+ /// See [`Self::group_by_role`].
149
+ #[serde(default)]
150
+ #[ts(as = "Option<_>")]
151
+ #[ts(optional)]
152
+ pub split_by_role: Option<String>,
153
+
154
+ /// `true` when this plugin CONNECTS its points in row order, so the
155
+ /// `View`'s row order is visible in the drawing: an unsorted config
156
+ /// renders the table's natural order, which reads as a tangle unless
157
+ /// the rows already arrive ordered along the axis. Declared rather
158
+ /// than inferred, because "unsorted" is not itself a mistake — a
159
+ /// pre-ordered table needs no `sort`, and the point plugins
160
+ /// (scatter, density) do not care at all.
161
+ #[serde(default)]
162
+ #[ts(as = "Option<_>")]
163
+ #[ts(optional)]
164
+ pub connects_row_order: bool,
123
165
  }
124
166
 
125
167
  impl PluginStaticConfig {
168
+ /// The number of leading `columns` slots that are POSITIONAL: a drop
169
+ /// there swaps with the column already present, and the slot's
170
+ /// meaning is fixed by [`Self::config_column_names`]. Everything from
171
+ /// this index on is the insert TAIL, which repeats the last named
172
+ /// role — which is why a `Y Line` (named slots: `["Y Axis"]`) takes
173
+ /// any number of columns as additional Y series, while an
174
+ /// `X/Y Line` (`["X Axis", "Y Axis", "Tooltip"]`) pins its two axes
175
+ /// and treats the rest as further tooltips.
176
+ ///
177
+ /// The single definition of that rule: [`Self::is_swap`] and the
178
+ /// agent's `list_plugins` contract both derive from it, so the
179
+ /// convention cannot be read two different ways.
180
+ pub fn positional_columns(&self) -> usize {
181
+ self.config_column_names.len().saturating_sub(1)
182
+ }
183
+
184
+ /// The role that `columns` past the positional slots repeat, i.e.
185
+ /// the last named slot. `None` when the plugin names no slots.
186
+ pub fn tail_column_role(&self) -> Option<&str> {
187
+ self.config_column_names.last().map(|x| x.as_str())
188
+ }
189
+
126
190
  /// `true` if dropping a column at `index` should swap with the
127
- /// column already there rather than insert. Only the named slots
128
- /// (`config_column_names[..len()-1]`) participate in swap behaviour;
129
- /// the trailing unnamed tail inserts.
191
+ /// column already there rather than insert.
130
192
  pub fn is_swap(&self, index: usize) -> bool {
131
- !self.config_column_names.is_empty() && index < self.config_column_names.len() - 1
193
+ !self.config_column_names.is_empty() && index < self.positional_columns()
132
194
  }
133
195
 
134
196
  pub fn get_group_rollups(&self, rollup_features: &[GroupRollupMode]) -> Vec<GroupRollupMode> {
@@ -141,4 +203,57 @@ impl PluginStaticConfig {
141
203
  })
142
204
  .unwrap_or_default()
143
205
  }
206
+
207
+ pub fn get_split_rollups(&self, rollup_features: &[SplitRollupMode]) -> Vec<SplitRollupMode> {
208
+ self.split_rollup_modes
209
+ .clone()
210
+ .map(|x| {
211
+ x.into_iter()
212
+ .filter(|y| rollup_features.is_empty() || rollup_features.contains(y))
213
+ .collect()
214
+ })
215
+ .unwrap_or_default()
216
+ }
217
+ }
218
+
219
+ #[cfg(test)]
220
+ mod tests {
221
+ use super::*;
222
+
223
+ fn plugin(names: &[&str]) -> PluginStaticConfig {
224
+ PluginStaticConfig {
225
+ config_column_names: names.iter().map(|x| (*x).to_owned()).collect(),
226
+ ..Default::default()
227
+ }
228
+ }
229
+
230
+ /// The tail rule has ONE definition: `is_swap` and the role a
231
+ /// trailing column repeats must never disagree about where the
232
+ /// positional slots end.
233
+ #[test]
234
+ fn positional_slots_and_tail_agree() {
235
+ // `Y Line` — the only named slot IS the tail, so every column
236
+ // is another Y series and none of them swap.
237
+ let y_line = plugin(&["Y Axis"]);
238
+ assert_eq!(y_line.positional_columns(), 0);
239
+ assert_eq!(y_line.tail_column_role(), Some("Y Axis"));
240
+ assert!(!y_line.is_swap(0));
241
+
242
+ // `X/Y Line` — two pinned axes, then tooltips.
243
+ let xy_line = plugin(&["X Axis", "Y Axis", "Tooltip"]);
244
+ assert_eq!(xy_line.positional_columns(), 2);
245
+ assert_eq!(xy_line.tail_column_role(), Some("Tooltip"));
246
+ assert!(xy_line.is_swap(0));
247
+ assert!(xy_line.is_swap(1));
248
+ assert!(!xy_line.is_swap(2));
249
+
250
+ for index in 0..4 {
251
+ assert_eq!(xy_line.is_swap(index), index < xy_line.positional_columns());
252
+ }
253
+
254
+ let unnamed = plugin(&[]);
255
+ assert_eq!(unnamed.positional_columns(), 0);
256
+ assert_eq!(unnamed.tail_column_role(), None);
257
+ assert!(!unnamed.is_swap(0));
258
+ }
144
259
  }
@@ -42,12 +42,42 @@ pub struct ViewerConfig<V: TS = String> {
42
42
  /// whole-element config format.
43
43
  #[derive(Debug, Default, Serialize, PartialEq, TS)]
44
44
  pub struct PanelViewerConfig<V: TS = String> {
45
+ /// The `@perspective-dev/viewer` version that wrote this config,
46
+ /// stamped on save and used to migrate older tokens. Callers do not
47
+ /// set it.
45
48
  pub version: V,
49
+
50
+ /// Per-column styling, keyed by column name. The viewer treats the
51
+ /// values as opaque — their shape is defined by the ACTIVE plugin,
52
+ /// so a config written for one plugin may carry keys another
53
+ /// ignores. Query the live shape with the `get_style_schema` agent
54
+ /// tool, or the plugin's `column_config_schema()`.
46
55
  pub columns_config: ColumnConfigMap,
56
+
57
+ /// Name of the visualization plugin, from the set registered on the
58
+ /// page (the `list_plugins` agent tool, or the plugin picker). This
59
+ /// also decides what the view fields MEAN visually: `columns` is
60
+ /// positional and each plugin reads the positions differently, and
61
+ /// `group_by`/`split_by` draw different things per plugin.
47
62
  pub plugin: String,
63
+
64
+ /// Plugin-wide settings (as opposed to the per-column
65
+ /// [`Self::columns_config`]). Opaque to the viewer and defined by
66
+ /// the active plugin; see `get_style_schema`.
48
67
  pub plugin_config: serde_json::Map<String, Value>,
49
- pub table: Option<String>,
68
+
69
+ /// Name of the `Table` this panel renders, as hosted on the panel's
70
+ /// `Client`. Every placed panel has a table binding (creation
71
+ /// requires one by type — [`ViewerConfigInitial`]), so the saved
72
+ /// config carries it unconditionally.
73
+ pub table: String,
74
+
75
+ /// Selected theme NAME (e.g. `"Pro Dark"`) — not a CSS value. Valid
76
+ /// names are the Perspective themes loaded on the page, which
77
+ /// `resetThemes()` re-scans. `None` selects the first available.
50
78
  pub theme: Option<String>,
79
+
80
+ /// Panel title, shown in its tab. `None` renders the default title.
51
81
  pub title: Option<String>,
52
82
 
53
83
  #[serde(flatten)]
@@ -93,41 +123,73 @@ impl Deref for PluginConfig {
93
123
 
94
124
  #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, TS)]
95
125
  pub struct ViewerConfigUpdate {
126
+ /// The `@perspective-dev/viewer` version a saved config was written
127
+ /// by, used to migrate older tokens. Omit it — the viewer stamps
128
+ /// the current version on save.
96
129
  #[serde(default)]
97
130
  #[ts(as = "Option<_>")]
98
131
  #[ts(optional)]
99
132
  pub version: VersionUpdate,
100
133
 
134
+ /// Name of the visualization plugin to switch to, from the set
135
+ /// registered on the page (the `list_plugins` agent tool, or the
136
+ /// plugin picker). Changing it changes what the view fields MEAN:
137
+ /// `columns` is positional and every plugin reads the positions
138
+ /// differently, and `group_by`/`split_by` draw different things per
139
+ /// plugin — so read the new plugin's roles before writing `columns`
140
+ /// for it.
101
141
  #[serde(default)]
102
142
  #[ts(as = "Option<_>")]
103
143
  #[ts(optional)]
104
144
  pub plugin: PluginUpdate,
105
145
 
146
+ /// Panel title, shown in its tab. `null` restores the default
147
+ /// title; omitting the field leaves the current one.
106
148
  #[serde(default)]
107
149
  #[ts(as = "Option<_>")]
108
150
  #[ts(optional)]
109
151
  pub title: TitleUpdate,
110
152
 
153
+ /// Name of the `Table` to render, as hosted on this panel's
154
+ /// `Client`. Rebinding an existing panel to another table keeps the
155
+ /// rest of the config, so column names that do not exist in the new
156
+ /// table will fail validation.
111
157
  #[serde(default)]
112
158
  #[ts(as = "Option<_>")]
113
159
  #[ts(optional)]
114
160
  pub table: TableUpdate,
115
161
 
162
+ /// Theme NAME (e.g. `"Pro Dark"`) — not a CSS value. Valid names are
163
+ /// the Perspective themes loaded on the page, re-scanned by
164
+ /// `resetThemes()`. `null` selects the default.
116
165
  #[serde(default)]
117
166
  #[ts(as = "Option<_>")]
118
167
  #[ts(optional)]
119
168
  pub theme: ThemeUpdate,
120
169
 
170
+ /// Whether the settings sidebar is OPEN. Purely cosmetic chrome —
171
+ /// it does not affect what the viewer renders, and it is
172
+ /// element-level rather than per-panel.
121
173
  #[serde(default)]
122
174
  #[ts(as = "Option<_>")]
123
175
  #[ts(optional)]
124
176
  pub settings: SettingsUpdate,
125
177
 
178
+ /// Plugin-wide settings (as opposed to the per-column
179
+ /// [`Self::columns_config`]). The viewer passes these through
180
+ /// opaquely — their valid keys are defined by the ACTIVE plugin and
181
+ /// vary by plugin and by state, so query them with the
182
+ /// `get_style_schema` agent tool rather than guessing.
126
183
  #[serde(default)]
127
184
  #[ts(as = "Option<_>")]
128
185
  #[ts(optional)]
129
186
  pub plugin_config: PluginConfigUpdate,
130
187
 
188
+ /// Per-column styling — formatting, colors, and other per-column
189
+ /// controls — keyed by column name. Opaque to the viewer and
190
+ /// plugin-defined like [`Self::plugin_config`]; `get_style_schema`
191
+ /// reports the valid keys for a given column under the active
192
+ /// plugin.
131
193
  #[serde(default)]
132
194
  #[ts(as = "Option<_>")]
133
195
  #[ts(optional)]
@@ -150,6 +212,119 @@ impl ViewerConfigUpdate {
150
212
  }
151
213
  }
152
214
 
215
+ /// The initial configuration of a NEW panel (`addPanel`, `restore`'s
216
+ /// panel-creating upsert, `restoreWorkspace` `panels` entries). Unlike
217
+ /// [`ViewerConfigUpdate`] — a patch against existing state — creation has
218
+ /// no prior state: `table` is REQUIRED (a placed panel without a table
219
+ /// binding would be permanently blank), absent fields mean "default"
220
+ /// rather than "leave unchanged" (so no [`OptionalUpdate`] tri-state), and
221
+ /// there is no `settings` field (element-level, not per-panel).
222
+ #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
223
+ pub struct ViewerConfigInitial {
224
+ /// Name of the `Table` the new panel renders, as hosted on the
225
+ /// `Client`. REQUIRED: a placed panel with no table binding would be
226
+ /// permanently blank.
227
+ pub table: String,
228
+
229
+ /// The `@perspective-dev/viewer` version a saved config was written
230
+ /// by. Omit it when creating a panel.
231
+ #[ts(optional)]
232
+ pub version: Option<String>,
233
+
234
+ /// Name of the visualization plugin, from the set registered on the
235
+ /// page (the `list_plugins` agent tool, or the plugin picker).
236
+ /// Decides what the view fields MEAN: `columns` is positional and
237
+ /// every plugin reads the positions differently, and
238
+ /// `group_by`/`split_by` draw different things per plugin. Absent
239
+ /// uses the default plugin.
240
+ #[ts(optional)]
241
+ pub plugin: Option<String>,
242
+
243
+ /// Panel title, shown in its tab. Absent renders the default title.
244
+ #[ts(optional)]
245
+ pub title: Option<String>,
246
+
247
+ /// Theme NAME (e.g. `"Pro Dark"`) — not a CSS value. Valid names are
248
+ /// the Perspective themes loaded on the page. Absent uses the
249
+ /// default.
250
+ #[ts(optional)]
251
+ pub theme: Option<String>,
252
+
253
+ /// Plugin-wide settings (as opposed to the per-column
254
+ /// [`Self::columns_config`]). Opaque to the viewer and defined by
255
+ /// the ACTIVE plugin; query the valid keys with `get_style_schema`
256
+ /// rather than guessing.
257
+ #[ts(optional)]
258
+ pub plugin_config: Option<serde_json::Map<String, Value>>,
259
+
260
+ /// Per-column styling — formatting, colors, and other per-column
261
+ /// controls — keyed by column name. Plugin-defined like
262
+ /// [`Self::plugin_config`]; see `get_style_schema`.
263
+ #[ts(optional)]
264
+ pub columns_config: Option<ColumnConfigMap>,
265
+
266
+ #[serde(flatten)]
267
+ pub view_config: ViewConfigUpdate,
268
+ }
269
+
270
+ impl ViewerConfigInitial {
271
+ /// Decode a `JsValue` by auto-detecting format from JavaScript type.
272
+ pub fn decode(config: &JsValue) -> ApiResult<Self> {
273
+ Ok(config.into_serde_ext()?)
274
+ }
275
+
276
+ /// A default-config panel bound to `table` (the "New panel" menus).
277
+ /// Exhaustive on purpose — same drift alarm as the `From` impl below.
278
+ pub fn new(table: impl Into<String>) -> Self {
279
+ Self {
280
+ table: table.into(),
281
+ version: None,
282
+ plugin: None,
283
+ title: None,
284
+ theme: None,
285
+ plugin_config: None,
286
+ columns_config: None,
287
+ view_config: ViewConfigUpdate::default(),
288
+ }
289
+ }
290
+ }
291
+
292
+ fn up<T: Clone>(value: Option<T>) -> OptionalUpdate<T> {
293
+ match value {
294
+ Some(value) => OptionalUpdate::Update(value),
295
+ None => OptionalUpdate::Missing,
296
+ }
297
+ }
298
+
299
+ // Constructed EXHAUSTIVELY on purpose: a field added to
300
+ // `ViewerConfigUpdate` breaks this impl at compile time, forcing the
301
+ // "should creation carry it?" decision (the drift alarm).
302
+ impl From<ViewerConfigInitial> for ViewerConfigUpdate {
303
+ fn from(value: ViewerConfigInitial) -> Self {
304
+ ViewerConfigUpdate {
305
+ version: up(value.version),
306
+ plugin: up(value.plugin),
307
+ title: up(value.title),
308
+ table: OptionalUpdate::Update(value.table),
309
+ theme: up(value.theme),
310
+ settings: OptionalUpdate::Missing,
311
+ plugin_config: up(value.plugin_config),
312
+ columns_config: up(value.columns_config),
313
+ view_config: value.view_config,
314
+ }
315
+ }
316
+ }
317
+
318
+ // There is deliberately NO `TryFrom<ViewerConfigUpdate>` here. Requiring a
319
+ // `table` is a property of the CREATION ENTRY POINTS — `addPanel`'s
320
+ // argument type, `WorkspaceConfigUpdate::panels`, and the agent's
321
+ // `add_panel` decode — each of which already has a `ViewerConfigInitial`
322
+ // in hand. An update→initial conversion exists only to let a route holding
323
+ // a PATCH pretend it is creating from scratch, which is how `restore`'s
324
+ // upsert acquired the gate and started rejecting the table-less
325
+ // restore-then-`load` contract. Its absence is what keeps that from
326
+ // recurring: `create_panel` takes an update, so no caller needs one.
327
+
153
328
  impl std::fmt::Display for ViewerConfigUpdate {
154
329
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155
330
  write!(
@@ -185,6 +360,50 @@ pub type VersionUpdate = OptionalUpdate<String>;
185
360
  pub type ColumnConfigUpdate = OptionalUpdate<ColumnConfigMap>;
186
361
  pub type PluginConfigUpdate = OptionalUpdate<serde_json::Map<String, Value>>;
187
362
 
363
+ #[cfg(test)]
364
+ mod tests {
365
+ use super::*;
366
+
367
+ #[test]
368
+ fn initial_requires_a_table() {
369
+ let json = serde_json::json!({ "group_by": ["State"] });
370
+ let err = serde_json::from_value::<ViewerConfigInitial>(json).unwrap_err();
371
+ assert!(format!("{err}").contains("table"));
372
+ }
373
+
374
+ /// The creation → patch direction is the only one that exists (see the
375
+ /// note above the `From` impl); a `table` becomes a concrete update and
376
+ /// the element-level `settings` is not carried.
377
+ #[test]
378
+ fn initial_widens_to_an_update() {
379
+ let json = serde_json::json!({
380
+ "table": "superstore",
381
+ "plugin": "Datagrid",
382
+ "group_by": ["State"],
383
+ });
384
+
385
+ let initial: ViewerConfigInitial = serde_json::from_value(json).unwrap();
386
+ let update = ViewerConfigUpdate::from(initial);
387
+ assert!(matches!(&update.table, OptionalUpdate::Update(x) if x == "superstore"));
388
+ assert!(matches!(&update.settings, OptionalUpdate::Missing));
389
+ assert!(matches!(&update.plugin, OptionalUpdate::Update(x) if x == "Datagrid"));
390
+ assert_eq!(
391
+ update.view_config.group_by.as_deref(),
392
+ Some(&["State".to_owned()][..])
393
+ );
394
+ }
395
+
396
+ /// A table-less patch is a legitimate creation input now that
397
+ /// `create_panel` takes an update — the deferred panel a `load()`
398
+ /// binds. Nothing in this module may reject it.
399
+ #[test]
400
+ fn a_table_less_update_is_representable() {
401
+ let json = serde_json::json!({ "group_by": ["State"] });
402
+ let update: ViewerConfigUpdate = serde_json::from_value(json).unwrap();
403
+ assert!(matches!(&update.table, OptionalUpdate::Missing));
404
+ }
405
+ }
406
+
188
407
  /// Handles `{}` when included as a field with `#[serde(default)]`.
189
408
  impl<T: Clone> Default for OptionalUpdate<T> {
190
409
  fn default() -> Self {