@carto/ps-react-ui 4.3.4 → 4.3.6

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 (67) hide show
  1. package/dist/components.js +2 -2
  2. package/dist/{error-B2IJ9d2h.js → error-piB8FwYO.js} +2 -2
  3. package/dist/{error-B2IJ9d2h.js.map → error-piB8FwYO.js.map} +1 -1
  4. package/dist/{lasso-tool-wFqOD6wk.js → lasso-tool-BctzdzBu.js} +185 -160
  5. package/dist/lasso-tool-BctzdzBu.js.map +1 -0
  6. package/dist/{no-data-C54XJt13.js → no-data-jdlbMef0.js} +2 -2
  7. package/dist/{no-data-C54XJt13.js.map → no-data-jdlbMef0.js.map} +1 -1
  8. package/dist/{row-DrHwXNvF.js → row-D3uVFImu.js} +2 -2
  9. package/dist/{row-DrHwXNvF.js.map → row-D3uVFImu.js.map} +1 -1
  10. package/dist/{series-D3Pc-kYX.js → series-BAImrSBo.js} +3 -3
  11. package/dist/{series-D3Pc-kYX.js.map → series-BAImrSBo.js.map} +1 -1
  12. package/dist/types/widgets/actions/index.d.ts +2 -2
  13. package/dist/types/widgets/actions/stack-toggle/stack-toggle.d.ts +3 -2
  14. package/dist/types/widgets/actions/zoom-toggle/zoom-toggle.d.ts +4 -0
  15. package/dist/types/widgets/echart/echart-ui.d.ts +1 -1
  16. package/dist/types/widgets/echart/types.d.ts +4 -0
  17. package/dist/types/widgets/loader/loader.d.ts +1 -1
  18. package/dist/types/widgets/loader/types.d.ts +1 -1
  19. package/dist/types/widgets/stores/index.d.ts +1 -1
  20. package/dist/types/widgets/stores/types.d.ts +15 -0
  21. package/dist/{use-widget-ref-B0aNCANx.js → use-widget-ref-B8x4sHIj.js} +2 -2
  22. package/dist/{use-widget-ref-B0aNCANx.js.map → use-widget-ref-B8x4sHIj.js.map} +1 -1
  23. package/dist/widget-store-Dn0Bnc4h.js +178 -0
  24. package/dist/widget-store-Dn0Bnc4h.js.map +1 -0
  25. package/dist/widgets/actions.js +698 -617
  26. package/dist/widgets/actions.js.map +1 -1
  27. package/dist/widgets/bar.js +2 -2
  28. package/dist/widgets/category.js +2 -2
  29. package/dist/widgets/echart.js +96 -85
  30. package/dist/widgets/echart.js.map +1 -1
  31. package/dist/widgets/error.js +1 -1
  32. package/dist/widgets/formula.js +5 -5
  33. package/dist/widgets/histogram.js +2 -2
  34. package/dist/widgets/loader.js +41 -40
  35. package/dist/widgets/loader.js.map +1 -1
  36. package/dist/widgets/markdown.js +2 -2
  37. package/dist/widgets/no-data.js +1 -1
  38. package/dist/widgets/pie.js +2 -2
  39. package/dist/widgets/range.js +2 -2
  40. package/dist/widgets/scatterplot.js +2 -2
  41. package/dist/widgets/skeleton-loader.js +1 -1
  42. package/dist/widgets/spread.js +5 -5
  43. package/dist/widgets/stores.js +1 -1
  44. package/dist/widgets/table.js +3 -3
  45. package/dist/widgets/timeseries.js +2 -2
  46. package/dist/widgets/wrapper.js +2 -2
  47. package/dist/widgets.js +4 -4
  48. package/package.json +1 -1
  49. package/src/components/lasso-tool/lasso-tool.tsx +5 -2
  50. package/src/widgets/actions/index.ts +2 -2
  51. package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +143 -9
  52. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +61 -70
  53. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +85 -53
  54. package/src/widgets/echart/echart-ui.test.tsx +18 -0
  55. package/src/widgets/echart/echart-ui.tsx +25 -13
  56. package/src/widgets/echart/echart.test.tsx +25 -0
  57. package/src/widgets/echart/echart.tsx +9 -1
  58. package/src/widgets/echart/types.ts +4 -0
  59. package/src/widgets/loader/loader.tsx +18 -8
  60. package/src/widgets/loader/types.ts +1 -1
  61. package/src/widgets/stores/index.ts +1 -0
  62. package/src/widgets/stores/types.ts +20 -0
  63. package/src/widgets/stores/widget-store.test.ts +141 -0
  64. package/src/widgets/stores/widget-store.ts +99 -2
  65. package/dist/lasso-tool-wFqOD6wk.js.map +0 -1
  66. package/dist/widget-store-CB6Trp_0.js +0 -131
  67. package/dist/widget-store-CB6Trp_0.js.map +0 -1
@@ -10,11 +10,17 @@ import { styles } from './style'
10
10
  import { Tooltip } from '../../../components'
11
11
  import { getEChartZoomConfig } from '../../echart/utils'
12
12
  import type { EchartWidgetState } from '../../echart/types'
13
+ import type { EchartOptionsProps } from '../../echart/types'
13
14
  import { useShallow } from 'zustand/shallow'
14
15
 
16
+ export const ZOOM_TOGGLE_TOOL_ID = 'zoom-toggle'
17
+
15
18
  /**
16
19
  * Widget action to toggle EChart zoom functionality.
17
20
  *
21
+ * Registers as a config pipeline tool so that zoom configuration is automatically
22
+ * re-applied when the base config is updated (e.g., by WidgetLoader).
23
+ *
18
24
  * When zoom is active, displays an inline reset button to disable zoom.
19
25
  * Only intended for use in fullscreen ToolbarActions for bar and histogram widgets.
20
26
  *
@@ -41,6 +47,10 @@ export function ZoomToggle({
41
47
  const theme = useTheme()
42
48
  const setWidget = useWidgetStore((state) => state.setWidget)
43
49
  const getWidget = useWidgetStore((state) => state.getWidget)
50
+ const registerTool = useWidgetStore((state) => state.registerTool)
51
+ const unregisterTool = useWidgetStore((state) => state.unregisterTool)
52
+ const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
53
+ const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
44
54
 
45
55
  const zoom = useWidgetStore(
46
56
  useShallow((state) => {
@@ -49,76 +59,94 @@ export function ZoomToggle({
49
59
  }),
50
60
  )
51
61
 
52
- const updateZoomOption = useCallback(
53
- (enabled: boolean, start: number, end: number) => {
54
- const zoomConfig = getEChartZoomConfig(
55
- enabled,
56
- { start, end },
57
- { inside: true, xSlider: true, ySlider: false },
58
- theme,
59
- )
60
- const currentOption = getWidget<EchartWidgetState>(id)?.option
61
-
62
- setWidget<EchartWidgetState & ZoomState>(id, {
63
- zoom: enabled,
64
- zoomStart: start,
65
- zoomEnd: end,
66
- option: {
67
- ...currentOption,
68
- ...zoomConfig,
69
- },
70
- })
71
- },
72
- [getWidget, id, setWidget, theme],
62
+ const zoomStart = useWidgetStore(
63
+ useShallow((state) => {
64
+ const widget = state.getWidget<ZoomState>(id)
65
+ return widget?.zoomStart ?? defaultZoomStart
66
+ }),
67
+ )
68
+
69
+ const zoomEnd = useWidgetStore(
70
+ useShallow((state) => {
71
+ const widget = state.getWidget<ZoomState>(id)
72
+ return widget?.zoomEnd ?? defaultZoomEnd
73
+ }),
73
74
  )
74
75
 
76
+ // Register config tool on mount
77
+ useEffect(() => {
78
+ registerTool(id, {
79
+ id: ZOOM_TOGGLE_TOOL_ID,
80
+ type: 'config',
81
+ order: 20,
82
+ enabled: zoom,
83
+ fn: (currentConfig, toolConfig) => {
84
+ const config = currentConfig as Record<string, unknown>
85
+ const option = config.option as EchartOptionsProps | undefined
86
+ const enabled = (toolConfig?.enabled as boolean) ?? false
87
+ const start = (toolConfig?.start as number) ?? 0
88
+ const end = (toolConfig?.end as number) ?? 100
89
+
90
+ const zoomConfig = getEChartZoomConfig(
91
+ enabled,
92
+ { start, end },
93
+ { inside: true, xSlider: true, ySlider: false },
94
+ theme,
95
+ )
96
+ return { ...config, option: { ...option, ...zoomConfig } }
97
+ },
98
+ config: { enabled: zoom, start: zoomStart, end: zoomEnd },
99
+ })
100
+ return () => unregisterTool(id, ZOOM_TOGGLE_TOOL_ID)
101
+ }, [id, registerTool, unregisterTool, zoom, theme, zoomStart, zoomEnd])
102
+
103
+ // Sync tool enabled/config when state changes
104
+ useEffect(() => {
105
+ setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, zoom)
106
+ updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
107
+ enabled: zoom,
108
+ start: zoomStart,
109
+ end: zoomEnd,
110
+ })
111
+ }, [id, zoom, zoomStart, zoomEnd, setToolEnabled, updateToolConfig])
112
+
75
113
  // Initialize zoom state from defaults only if not already set
76
114
  useEffect(() => {
77
115
  const existingState = getWidget<ZoomState>(id)
78
116
 
79
117
  if (existingState?.zoom) return
80
118
 
81
- // Apply initial zoom config
82
- updateZoomOption(
83
- existingState?.zoom ?? defaultZoom,
84
- existingState?.zoomStart ?? defaultZoomStart,
85
- existingState?.zoomEnd ?? defaultZoomEnd,
86
- )
87
- }, [
88
- defaultZoom,
89
- defaultZoomEnd,
90
- defaultZoomStart,
91
- getWidget,
92
- id,
93
- updateZoomOption,
94
- ])
95
-
96
- // Cleanup: reset zoom when component unmounts
119
+ setWidget<ZoomState>(id, {
120
+ zoom: existingState?.zoom ?? defaultZoom,
121
+ zoomStart: existingState?.zoomStart ?? defaultZoomStart,
122
+ zoomEnd: existingState?.zoomEnd ?? defaultZoomEnd,
123
+ })
124
+ }, [defaultZoom, defaultZoomEnd, defaultZoomStart, getWidget, id, setWidget])
125
+
126
+ // Cleanup: disable zoom when component unmounts
97
127
  useEffect(() => {
98
128
  return () => {
99
- const existingState = getWidget<ZoomState>(id)
100
- updateZoomOption(
101
- false,
102
- existingState?.zoomStart ?? 0,
103
- existingState?.zoomEnd ?? 100,
104
- )
129
+ setWidget<ZoomState>(id, { zoom: false })
105
130
  }
106
- }, [getWidget, id, updateZoomOption])
131
+ }, [id, setWidget])
107
132
 
108
133
  const handleToggle = () => {
109
134
  const newZoom = !zoom
110
-
111
135
  const existingState = getWidget<ZoomState>(id)
112
136
 
113
- updateZoomOption(
114
- newZoom,
115
- newZoom ? (existingState?.zoomStart ?? defaultZoomStart) : 0,
116
- newZoom ? (existingState?.zoomEnd ?? defaultZoomEnd) : 100,
117
- )
137
+ setWidget<ZoomState>(id, {
138
+ zoom: newZoom,
139
+ zoomStart: newZoom ? (existingState?.zoomStart ?? defaultZoomStart) : 0,
140
+ zoomEnd: newZoom ? (existingState?.zoomEnd ?? defaultZoomEnd) : 100,
141
+ })
118
142
  }
119
143
 
120
144
  const handleReset = () => {
121
- updateZoomOption(true, defaultZoomStart, defaultZoomEnd)
145
+ setWidget<ZoomState>(id, {
146
+ zoom: true,
147
+ zoomStart: defaultZoomStart,
148
+ zoomEnd: defaultZoomEnd,
149
+ })
122
150
  }
123
151
 
124
152
  // Handle dataZoom event to update zoom range in store
@@ -133,10 +161,14 @@ export function ZoomToggle({
133
161
  const end = zoomEvent.end
134
162
 
135
163
  if (start !== undefined && end !== undefined) {
136
- updateZoomOption(true, start, end)
164
+ setWidget<ZoomState>(id, {
165
+ zoom: true,
166
+ zoomStart: start,
167
+ zoomEnd: end,
168
+ })
137
169
  }
138
170
  },
139
- [updateZoomOption],
171
+ [id, setWidget],
140
172
  )
141
173
 
142
174
  // Register dataZoom event handler when zoom is enabled
@@ -175,6 +175,7 @@ describe('EchartUI', () => {
175
175
 
176
176
  expect(mockChart.setOption).toHaveBeenCalledWith(basicOption, {
177
177
  lazyUpdate: true,
178
+ replaceMerge: ['dataset', 'series'],
178
179
  })
179
180
  })
180
181
 
@@ -199,6 +200,22 @@ describe('EchartUI', () => {
199
200
 
200
201
  expect(mockChart.setOption).toHaveBeenCalledWith(newOption, {
201
202
  lazyUpdate: true,
203
+ replaceMerge: ['dataset', 'series'],
204
+ })
205
+ })
206
+
207
+ test('uses provided replaceMerge values', () => {
208
+ render(
209
+ <EchartUI
210
+ {...defaultProps}
211
+ option={basicOption}
212
+ replaceMerge={['series']}
213
+ />,
214
+ )
215
+
216
+ expect(mockChart.setOption).toHaveBeenCalledWith(basicOption, {
217
+ lazyUpdate: true,
218
+ replaceMerge: ['series'],
202
219
  })
203
220
  })
204
221
 
@@ -479,6 +496,7 @@ describe('EchartUI', () => {
479
496
 
480
497
  expect(mockChart.setOption).toHaveBeenCalledWith(complexOption, {
481
498
  lazyUpdate: true,
499
+ replaceMerge: ['dataset', 'series'],
482
500
  })
483
501
  })
484
502
 
@@ -3,15 +3,14 @@ import * as echarts from 'echarts'
3
3
  import type { EchartUIProps } from './types'
4
4
  import { useWidgetRef } from '../../hooks'
5
5
 
6
- export function EchartUI({
7
- id,
8
- ref,
9
- init,
10
- option,
11
- className,
12
- style,
13
- onEvents,
14
- }: EchartUIProps) {
6
+ const DEFAULT_REPLACE_MERGE: string[] = ['dataset', 'series']
7
+
8
+ export function EchartUI(props: EchartUIProps) {
9
+ const { id, ref, init, option, className, style, onEvents } = props
10
+ const replaceMerge = toReplaceMerge(
11
+ (props as { replaceMerge?: unknown }).replaceMerge,
12
+ )
13
+
15
14
  const chartRef = useWidgetRef<HTMLDivElement>(id)
16
15
  const chartInstance = useRef<echarts.ECharts>(null)
17
16
  const resizeObserverRef = useRef<ResizeObserver | null>(null)
@@ -35,11 +34,16 @@ export function EchartUI({
35
34
 
36
35
  // Update chart when options change
37
36
  useEffect(() => {
38
- chartInstance.current?.setOption(option, {
37
+ const setOptionConfig = {
39
38
  lazyUpdate: true,
40
- // notMerge: true,
41
- })
42
- }, [option])
39
+ replaceMerge: replaceMerge ?? DEFAULT_REPLACE_MERGE,
40
+ }
41
+
42
+ chartInstance.current?.setOption(
43
+ option,
44
+ setOptionConfig as Parameters<echarts.ECharts['setOption']>[1],
45
+ )
46
+ }, [option, replaceMerge])
43
47
 
44
48
  // Handle resize using ResizeObserver
45
49
  useEffect(() => {
@@ -78,3 +82,11 @@ export function EchartUI({
78
82
 
79
83
  return <div id={id} ref={chartRef} style={style} className={className} />
80
84
  }
85
+
86
+ function toReplaceMerge(value: unknown): string[] | undefined {
87
+ if (!Array.isArray(value)) {
88
+ return undefined
89
+ }
90
+
91
+ return value.filter((item): item is string => typeof item === 'string')
92
+ }
@@ -184,6 +184,7 @@ describe('Echart', () => {
184
184
  }),
185
185
  {
186
186
  lazyUpdate: true,
187
+ replaceMerge: ['dataset', 'series'],
187
188
  },
188
189
  )
189
190
  })
@@ -276,6 +277,7 @@ describe('Echart', () => {
276
277
 
277
278
  expect(mockChart.setOption).toHaveBeenCalledWith(basicOption, {
278
279
  lazyUpdate: true,
280
+ replaceMerge: ['dataset', 'series'],
279
281
  })
280
282
 
281
283
  const newOption: EChartsOption = {
@@ -297,6 +299,7 @@ describe('Echart', () => {
297
299
 
298
300
  expect(mockChart.setOption).toHaveBeenCalledWith(newOption, {
299
301
  lazyUpdate: true,
302
+ replaceMerge: ['dataset', 'series'],
300
303
  })
301
304
  })
302
305
 
@@ -364,6 +367,7 @@ describe('Echart', () => {
364
367
 
365
368
  expect(mockChart.setOption).toHaveBeenCalledWith(complexOption, {
366
369
  lazyUpdate: true,
370
+ replaceMerge: ['dataset', 'series'],
367
371
  })
368
372
  })
369
373
 
@@ -400,6 +404,7 @@ describe('Echart', () => {
400
404
  )
401
405
  expect(mockChart.setOption).toHaveBeenCalledWith(basicOption, {
402
406
  lazyUpdate: true,
407
+ replaceMerge: ['dataset', 'series'],
403
408
  })
404
409
  expect(mockChart.on).toHaveBeenCalledWith('click', clickHandler)
405
410
  })
@@ -442,6 +447,7 @@ describe('Echart', () => {
442
447
 
443
448
  expect(mockChart.setOption).toHaveBeenCalledWith(emptyOption, {
444
449
  lazyUpdate: true,
450
+ replaceMerge: ['dataset', 'series'],
445
451
  })
446
452
  })
447
453
 
@@ -464,6 +470,7 @@ describe('Echart', () => {
464
470
  }),
465
471
  {
466
472
  lazyUpdate: true,
473
+ replaceMerge: ['dataset', 'series'],
467
474
  },
468
475
  )
469
476
  })
@@ -509,6 +516,7 @@ describe('Echart', () => {
509
516
 
510
517
  expect(mockChart.setOption).toHaveBeenCalledWith(basicOption, {
511
518
  lazyUpdate: true,
519
+ replaceMerge: ['dataset', 'series'],
512
520
  })
513
521
 
514
522
  rerender(<Echart id='chart-b' />)
@@ -517,10 +525,27 @@ describe('Echart', () => {
517
525
  { series: [{ type: 'line', data: [1, 2, 3] }] },
518
526
  {
519
527
  lazyUpdate: true,
528
+ replaceMerge: ['dataset', 'series'],
520
529
  },
521
530
  )
522
531
  })
523
532
 
533
+ test('passes replaceMerge from widget to EchartUI', () => {
534
+ useWidgetStore.getState().setWidget('test-echart', {
535
+ id: 'test-echart',
536
+ type: 'echart',
537
+ option: basicOption,
538
+ replaceMerge: ['series'],
539
+ } as EchartWidgetState)
540
+
541
+ render(<Echart id='test-echart' />)
542
+
543
+ expect(mockChart.setOption).toHaveBeenCalledWith(basicOption, {
544
+ lazyUpdate: true,
545
+ replaceMerge: ['series'],
546
+ })
547
+ })
548
+
524
549
  test('disposes chart when component unmounts', () => {
525
550
  useWidgetStore.getState().setWidget('test-echart', {
526
551
  id: 'test-echart',
@@ -19,6 +19,7 @@ export function Echart(props: EchartProps) {
19
19
  option: widget?.option,
20
20
  onEvents: widget?.onEvents,
21
21
  init: widget?.init,
22
+ replaceMerge: widget?.replaceMerge,
22
23
  }
23
24
  }),
24
25
  )
@@ -36,9 +37,16 @@ export function Echart(props: EchartProps) {
36
37
 
37
38
  const onEvents = widget.onEvents
38
39
  const init = widget.init
40
+ const replaceMerge = widget.replaceMerge
39
41
 
40
42
  return (
41
- <EchartUI id={props.id} option={option} onEvents={onEvents} init={init} />
43
+ <EchartUI
44
+ id={props.id}
45
+ option={option}
46
+ onEvents={onEvents}
47
+ init={init}
48
+ replaceMerge={replaceMerge}
49
+ />
42
50
  )
43
51
  }
44
52
 
@@ -5,12 +5,14 @@ import type { Ref } from 'react'
5
5
  import { theme as CartoTheme } from '@carto/meridian-ds/theme'
6
6
 
7
7
  export type EchartOptionsProps = EChartsOption
8
+ export type EchartReplaceMerge = string[]
8
9
 
9
10
  export interface EchartUIProps {
10
11
  id: string
11
12
  option: EchartOptionsProps
12
13
  className?: string
13
14
  init?: echarts.EChartsInitOpts
15
+ replaceMerge?: EchartReplaceMerge
14
16
  style?: React.CSSProperties
15
17
  ref?: Ref<echarts.ECharts>
16
18
  onEvents?: Record<string, Parameters<echarts.ECharts['on']>[2]>
@@ -26,6 +28,7 @@ export type EchartWidgetState = BaseWidgetState<{
26
28
  option: EchartUIProps['option']
27
29
  onEvents?: EchartUIProps['onEvents']
28
30
  init?: EchartUIProps['init']
31
+ replaceMerge?: EchartReplaceMerge
29
32
  }>
30
33
 
31
34
  export interface EchartWidgetOptionProps<D> {
@@ -38,4 +41,5 @@ export interface EchartWidgetProps {
38
41
  type: string
39
42
  option: EchartUIProps['option']
40
43
  onEvents?: EchartUIProps['onEvents']
44
+ replaceMerge?: EchartReplaceMerge
41
45
  }
@@ -3,11 +3,14 @@ import type { WidgetLoaderProps } from './types'
3
3
  import { useWidgetStore } from '../stores/widget-store'
4
4
  import type { WrapperState } from '../wrapper'
5
5
 
6
- export function WidgetLoader<T>(props: WidgetLoaderProps<T>) {
6
+ export function WidgetLoader<T extends Record<string, unknown> = Record<string, unknown>>(props: WidgetLoaderProps<T>) {
7
7
  const setWidget = useWidgetStore((state) => state.setWidget)
8
8
  const executeToolPipeline = useWidgetStore(
9
9
  (state) => state.executeToolPipeline,
10
10
  )
11
+ const executeConfigPipeline = useWidgetStore(
12
+ (state) => state.executeConfigPipeline,
13
+ )
11
14
 
12
15
  // Split into 3 effects for metadata and 1 for data pipeline:
13
16
  // Each property that can be modified independently gets its own effect to avoid
@@ -35,21 +38,19 @@ export function WidgetLoader<T>(props: WidgetLoaderProps<T>) {
35
38
  })
36
39
  }, [props.id, props.isLoading, props.isFetching, props.error, setWidget])
37
40
 
38
- // Effect 3: Config updates
41
+ // Effect 3: Config updates — run through config pipeline
39
42
  useEffect(() => {
40
43
  if (props.config) {
41
- setWidget<WrapperState>(props.id, {
42
- ...props.config,
43
- })
44
+ void executeConfigPipeline(props.id, props.config)
44
45
  }
45
- }, [props.id, props.config, setWidget])
46
+ }, [props.id, props.config, executeConfigPipeline])
46
47
 
47
48
  // Effect 4: Execute tool pipeline when props.data changes
48
49
  useEffect(() => {
49
50
  void executeToolPipeline(props.id, props.data)
50
51
  }, [props.id, props.data, executeToolPipeline])
51
52
 
52
- // Effect 5: Re-execute pipeline when tool state changes (enabled/config)
53
+ // Effect 5: Re-execute pipelines when tool state changes (enabled/config)
53
54
  useEffect(() => {
54
55
  let prevTools = useWidgetStore.getState().widgets[props.id]?.registeredTools
55
56
 
@@ -60,11 +61,20 @@ export function WidgetLoader<T>(props: WidgetLoaderProps<T>) {
60
61
  if (currentTools !== prevTools) {
61
62
  prevTools = currentTools
62
63
  void executeToolPipeline(props.id, props.data)
64
+ if (props.config) {
65
+ void executeConfigPipeline(props.id, props.config)
66
+ }
63
67
  }
64
68
  })
65
69
 
66
70
  return unsubscribe
67
- }, [props.id, props.data, executeToolPipeline])
71
+ }, [
72
+ props.id,
73
+ props.data,
74
+ props.config,
75
+ executeToolPipeline,
76
+ executeConfigPipeline,
77
+ ])
68
78
 
69
79
  return props.children
70
80
  }
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from 'react'
2
2
  import type { WidgetsStoreProps, WidgetState } from '../stores/types'
3
3
 
4
- export interface WidgetLoaderProps<T> extends WidgetsStoreProps {
4
+ export interface WidgetLoaderProps<T extends Record<string, unknown> = Record<string, unknown>> extends WidgetsStoreProps {
5
5
  children: ReactNode
6
6
  config?: T
7
7
  }
@@ -1,6 +1,7 @@
1
1
  export { useWidgetStore } from './widget-store'
2
2
  export type {
3
3
  BaseWidgetState,
4
+ ToolType,
4
5
  WidgetsStoreProps,
5
6
  WidgetState,
6
7
  WidgetStore,
@@ -1,5 +1,12 @@
1
1
  import type { RefObject } from 'react'
2
2
 
3
+ /**
4
+ * Tool type determines which pipeline a tool participates in.
5
+ * - 'data': transforms widget data (default)
6
+ * - 'config': transforms widget config/option
7
+ */
8
+ export type ToolType = 'data' | 'config'
9
+
3
10
  export interface WidgetsStoreProps {
4
11
  /** Unique identifier for the widget */
5
12
  id: string
@@ -77,6 +84,8 @@ export interface ToolRegistration {
77
84
  fn: ToolTransformFunction
78
85
  /** Whether tool is currently enabled */
79
86
  enabled: boolean
87
+ /** 'data' (default) transforms data, 'config' transforms widget config/option */
88
+ type?: ToolType
80
89
  /** Tool-specific configuration */
81
90
  config?: Record<string, unknown>
82
91
  /**
@@ -184,6 +193,17 @@ export interface WidgetStoreActions {
184
193
  * @param sourceData - Original data to transform
185
194
  */
186
195
  executeToolPipeline: (widgetId: string, sourceData: unknown) => Promise<void>
196
+
197
+ /**
198
+ * Execute the config transformation pipeline
199
+ * Applies config-type tools to the base config, then sets the result on the widget
200
+ * @param widgetId - Widget ID
201
+ * @param baseConfig - Base config to transform
202
+ */
203
+ executeConfigPipeline: (
204
+ widgetId: string,
205
+ baseConfig: Record<string, unknown>,
206
+ ) => Promise<void>
187
207
  }
188
208
 
189
209
  /**