1- import { useState , type CSSProperties , type ReactNode } from "react" ;
1+ import { useRef , useState , type CSSProperties , type ReactNode } from "react" ;
22
33import { Badge } from "@/components/ui/badge" ;
44import { Button } from "@/components/ui/button" ;
@@ -10,7 +10,7 @@ import { Textarea } from "@/components/ui/textarea";
1010import { getCustomComponent } from "@/components/widgets/customComponentRegistry" ;
1111import { cn } from "@/lib/utils" ;
1212import { dispatchWidgetUiEvent } from "@/lib/widgetUiEvents" ;
13- import { useWidgetSession } from "@/lib/widgetSession" ;
13+ import { useWidgetSession , useWidgetUiValueChange } from "@/lib/widgetSession" ;
1414
1515export type WidgetEventBindings = Record < string , string > ;
1616
@@ -53,7 +53,9 @@ export function WidgetHost({
5353 descriptor : WidgetDescriptor ;
5454} ) {
5555 const contextSessionId = useWidgetSession ( ) ;
56+ const onUiValueChange = useWidgetUiValueChange ( ) ;
5657 const resolvedSessionId = sessionId ?? contextSessionId ;
58+ const uiValueTimer = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
5759 const dispatchEvent = async ( callbackId : string | undefined , eventType : string , payload : unknown ) => {
5860 if ( ! callbackId || ! resolvedSessionId ) return ;
5961 try {
@@ -69,25 +71,38 @@ export function WidgetHost({
6971 }
7072 } ;
7173
72- return < WidgetNode descriptor = { descriptor } dispatchEvent = { dispatchEvent } /> ;
74+ // 값 위젯(elementId 보유) 변경 → 그 변수를 쓰는 셀만 리액티브 갱신. 슬라이더 드래그
75+ // hammering을 막기 위해 150ms 디바운스(마지막 값만 보낸다).
76+ const dispatchUiValue = ( elementId : string , value : unknown ) => {
77+ if ( ! onUiValueChange || ! elementId ) return ;
78+ if ( uiValueTimer . current ) clearTimeout ( uiValueTimer . current ) ;
79+ uiValueTimer . current = setTimeout ( ( ) => {
80+ void onUiValueChange ( { blockId : blockId ?? null , elementId, value } ) ;
81+ } , 150 ) ;
82+ } ;
83+
84+ return < WidgetNode descriptor = { descriptor } dispatchEvent = { dispatchEvent } dispatchUiValue = { dispatchUiValue } /> ;
7385}
7486
7587type Dispatch = ( callbackId : string | undefined , eventType : string , payload : unknown ) => Promise < void > ;
88+ type DispatchUiValue = ( elementId : string , value : unknown ) => void ;
7689
7790function WidgetNode ( {
7891 descriptor,
7992 dispatchEvent,
93+ dispatchUiValue,
8094} : {
8195 descriptor : WidgetDescriptor ;
8296 dispatchEvent : Dispatch ;
97+ dispatchUiValue : DispatchUiValue ;
8398} ) {
8499 if ( descriptor . type === "ui" ) {
85- return < UiWidget descriptor = { descriptor } dispatchEvent = { dispatchEvent } /> ;
100+ return < UiWidget descriptor = { descriptor } dispatchEvent = { dispatchEvent } dispatchUiValue = { dispatchUiValue } /> ;
86101 }
87102 if ( descriptor . type === "custom" ) {
88103 return < CustomWidget descriptor = { descriptor } /> ;
89104 }
90- return < ContainerWidget descriptor = { descriptor } dispatchEvent = { dispatchEvent } /> ;
105+ return < ContainerWidget descriptor = { descriptor } dispatchEvent = { dispatchEvent } dispatchUiValue = { dispatchUiValue } /> ;
91106}
92107
93108function CustomWidget ( { descriptor } : { descriptor : WidgetDescriptor } ) {
@@ -115,9 +130,11 @@ function CustomWidget({ descriptor }: { descriptor: WidgetDescriptor }) {
115130function ContainerWidget ( {
116131 descriptor,
117132 dispatchEvent,
133+ dispatchUiValue,
118134} : {
119135 descriptor : WidgetDescriptor ;
120136 dispatchEvent : Dispatch ;
137+ dispatchUiValue : DispatchUiValue ;
121138} ) {
122139 const renderChild = ( child : unknown , key : number ) : ReactNode => {
123140 if ( isWidgetDescriptor ( child ) ) {
@@ -126,6 +143,7 @@ function ContainerWidget({
126143 key = { key }
127144 descriptor = { child }
128145 dispatchEvent = { dispatchEvent }
146+ dispatchUiValue = { dispatchUiValue }
129147 />
130148 ) ;
131149 }
@@ -196,7 +214,7 @@ function ContainerWidget({
196214 >
197215 { title ? < div className = "mb-1 text-xs font-semibold uppercase" > { title } </ div > : null }
198216 { isWidgetDescriptor ( content ) ? (
199- < WidgetNode descriptor = { content } dispatchEvent = { dispatchEvent } />
217+ < WidgetNode descriptor = { content } dispatchEvent = { dispatchEvent } dispatchUiValue = { dispatchUiValue } />
200218 ) : (
201219 < div > { String ( content ?? "" ) } </ div >
202220 ) }
@@ -215,7 +233,7 @@ function ContainerWidget({
215233 < summary className = "cursor-pointer px-3 py-2 text-sm font-medium" > { entry . label } </ summary >
216234 < div className = "px-3 pb-3" >
217235 { isWidgetDescriptor ( entry . content ) ? (
218- < WidgetNode descriptor = { entry . content } dispatchEvent = { dispatchEvent } />
236+ < WidgetNode descriptor = { entry . content } dispatchEvent = { dispatchEvent } dispatchUiValue = { dispatchUiValue } />
219237 ) : (
220238 < div className = "text-sm" > { String ( entry . content ?? "" ) } </ div >
221239 ) }
@@ -243,7 +261,7 @@ function ContainerWidget({
243261 { items . map ( ( entry ) => (
244262 < TabsContent key = { entry . label } value = { entry . label } >
245263 { isWidgetDescriptor ( entry . content ) ? (
246- < WidgetNode descriptor = { entry . content } dispatchEvent = { dispatchEvent } />
264+ < WidgetNode descriptor = { entry . content } dispatchEvent = { dispatchEvent } dispatchUiValue = { dispatchUiValue } />
247265 ) : (
248266 < div className = "text-sm" > { String ( entry . content ?? "" ) } </ div >
249267 ) }
@@ -261,11 +279,11 @@ function ContainerWidget({
261279 data-widget = "sidebar"
262280 style = { { gridTemplateColumns : String ( ( descriptor as { width ?: string } ) . width ?? "minmax(0,1fr)" ) } }
263281 >
264- { isWidgetDescriptor ( content ) ? < WidgetNode descriptor = { content } dispatchEvent = { dispatchEvent } /> : null }
282+ { isWidgetDescriptor ( content ) ? < WidgetNode descriptor = { content } dispatchEvent = { dispatchEvent } dispatchUiValue = { dispatchUiValue } /> : null }
265283 { footer ? (
266284 < div className = "border-t pt-2 text-xs text-muted-foreground" >
267285 { isWidgetDescriptor ( footer ) ? (
268- < WidgetNode descriptor = { footer } dispatchEvent = { dispatchEvent } />
286+ < WidgetNode descriptor = { footer } dispatchEvent = { dispatchEvent } dispatchUiValue = { dispatchUiValue } />
269287 ) : (
270288 < div > { String ( footer ?? "" ) } </ div >
271289 ) }
@@ -307,13 +325,22 @@ function ContainerWidget({
307325function UiWidget ( {
308326 descriptor,
309327 dispatchEvent,
328+ dispatchUiValue,
310329} : {
311330 descriptor : WidgetDescriptor ;
312331 dispatchEvent : Dispatch ;
332+ dispatchUiValue : DispatchUiValue ;
313333} ) {
314334 const component = descriptor . component ?? "" ;
315335 const events = ( descriptor . events ?? { } ) as WidgetEventBindings ;
316336 const label = String ( ( descriptor as { label ?: string } ) . label ?? "" ) ;
337+ const elementId = String ( ( descriptor as { elementId ?: unknown } ) . elementId ?? "" ) ;
338+
339+ // 값 변경 = 옵션 콜백(기존) + 리액티브 값-바인딩(elementId 있을 때).
340+ const emitChange = ( value : unknown ) => {
341+ void dispatchEvent ( events . change , "change" , value ) ;
342+ if ( elementId ) dispatchUiValue ( elementId , value ) ;
343+ } ;
317344
318345 switch ( component ) {
319346 case "button" : {
@@ -363,7 +390,7 @@ function UiWidget({
363390 min = { numberOrUndefined ( ( descriptor as { min ?: unknown } ) . min ) }
364391 max = { numberOrUndefined ( ( descriptor as { max ?: unknown } ) . max ) }
365392 step = { numberOrUndefined ( ( descriptor as { step ?: unknown } ) . step ) }
366- onChange = { ( event ) => dispatchEvent ( events . change , "change" , Number ( event . target . value ) ) }
393+ onChange = { ( event ) => emitChange ( Number ( event . target . value ) ) }
367394 />
368395 </ UiInputWrapper >
369396 ) ;
@@ -378,7 +405,7 @@ function UiWidget({
378405 min = { Number ( ( descriptor as { min ?: unknown } ) . min ?? 0 ) }
379406 max = { Number ( ( descriptor as { max ?: unknown } ) . max ?? 100 ) }
380407 step = { Number ( ( descriptor as { step ?: unknown } ) . step ?? 1 ) }
381- onChange = { ( event ) => dispatchEvent ( events . change , "change" , Number ( event . target . value ) ) }
408+ onChange = { ( event ) => emitChange ( Number ( event . target . value ) ) }
382409 />
383410 </ UiInputWrapper >
384411 ) ;
@@ -389,7 +416,7 @@ function UiWidget({
389416 < input
390417 type = "checkbox"
391418 defaultChecked = { Boolean ( ( descriptor as { value ?: unknown } ) . value ) }
392- onChange = { ( event ) => dispatchEvent ( events . change , "change" , event . target . checked ) }
419+ onChange = { ( event ) => emitChange ( event . target . checked ) }
393420 />
394421 < span > { label } </ span >
395422 </ label >
@@ -403,7 +430,7 @@ function UiWidget({
403430 data-widget-ui = "dropdown"
404431 className = "h-9 w-full rounded-md border bg-background px-2 text-sm"
405432 defaultValue = { value }
406- onChange = { ( event ) => dispatchEvent ( events . change , "change" , event . target . value ) }
433+ onChange = { ( event ) => emitChange ( event . target . value ) }
407434 >
408435 { options . map ( ( option ) => (
409436 < option key = { option } value = { option } >
0 commit comments