syd 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl

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.
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, Dict, Generic, List, TypeVar, Union, Optional
2
+ from typing import Any, Dict, Generic, List, TypeVar, Union, Callable, Optional
3
3
  from dataclasses import dataclass
4
- from markupsafe import Markup
4
+ from html import escape
5
5
 
6
6
  from ..parameters import (
7
7
  Parameter,
@@ -15,402 +15,480 @@ from ..parameters import (
15
15
  FloatRangeParameter,
16
16
  UnboundedIntegerParameter,
17
17
  UnboundedFloatParameter,
18
- ButtonParameter,
18
+ ButtonAction,
19
19
  )
20
20
 
21
21
  T = TypeVar("T", bound=Parameter[Any])
22
22
 
23
23
 
24
- class BaseWebComponent(Generic[T], ABC):
25
- """
26
- Abstract base class for all web components.
24
+ @dataclass
25
+ class ComponentStyle:
26
+ """Style configuration for components."""
27
+
28
+ width: str = "auto"
29
+ margin: str = "3px 0px"
30
+ description_width: str = "initial"
31
+ input_class: str = "form-control"
32
+ label_class: str = "form-label"
33
+ container_class: str = "mb-3"
34
+
27
35
 
28
- This class defines the interface for HTML/JS components that correspond
29
- to different parameter types in the web deployment.
36
+ class BaseComponent(Generic[T], ABC):
30
37
  """
38
+ Abstract base class for all parameter components.
31
39
 
32
- def __init__(self, parameter: T, component_id: str):
33
- """
34
- Initialize the web component.
40
+ This class defines the common interface and shared functionality
41
+ for components that correspond to different parameter types.
42
+ """
35
43
 
36
- Args:
37
- parameter: The parameter this component represents
38
- component_id: Unique ID for this component in the DOM
39
- """
40
- self.parameter = parameter
41
- self.component_id = component_id
44
+ _callbacks: List[Callable]
45
+ _is_action: bool = False
46
+ _value: Any
47
+ _id: str
48
+
49
+ def __init__(
50
+ self,
51
+ parameter: T,
52
+ continuous: bool = False,
53
+ style: Optional[ComponentStyle] = None,
54
+ ):
55
+ self._id = f"param_{parameter.name}"
56
+ self._value = parameter.value
57
+ self._callbacks = []
58
+ self._continuous = continuous
59
+ self._style = style or ComponentStyle()
60
+ self._html = self._create_html(parameter)
42
61
 
43
62
  @abstractmethod
44
- def render_html(self) -> str:
45
- """
46
- Render the HTML for this component.
63
+ def _create_html(self, parameter: T) -> str:
64
+ """Create and return the appropriate HTML markup."""
65
+ pass
47
66
 
48
- Returns:
49
- str: HTML markup for the component
50
- """
67
+ @property
68
+ def html(self) -> str:
69
+ """Get the HTML representation of the component."""
70
+ return self._html
71
+
72
+ @property
73
+ def value(self) -> Any:
74
+ """Get the current value of the component."""
75
+ return self._value
76
+
77
+ @value.setter
78
+ def value(self, new_value: Any) -> None:
79
+ """Set the value of the component."""
80
+ self._value = new_value
81
+ # In a real implementation, we'd use JavaScript to update the DOM
82
+ # This would be handled by the Flask app's frontend code
83
+
84
+ def matches_parameter(self, parameter: T) -> bool:
85
+ """Check if the component matches the parameter."""
86
+ return self.value == parameter.value
87
+
88
+ def update_from_parameter(self, parameter: T) -> None:
89
+ """Update the component from the parameter."""
90
+ try:
91
+ self.disable_callbacks()
92
+ self.extra_updates_from_parameter(parameter)
93
+ self.value = parameter.value
94
+ finally:
95
+ self.reenable_callbacks()
96
+
97
+ def extra_updates_from_parameter(self, parameter: T) -> None:
98
+ """Extra updates from the parameter."""
51
99
  pass
52
100
 
53
- @abstractmethod
54
- def get_js_init(self) -> str:
55
- """
56
- Get JavaScript code needed to initialize this component.
101
+ def observe(self, callback: Callable) -> None:
102
+ """Register a callback for value changes."""
103
+ self._callbacks.append(callback)
57
104
 
58
- Returns:
59
- str: JavaScript code that sets up event listeners etc.
60
- """
61
- pass
105
+ def unobserve(self, callback: Callable) -> None:
106
+ """Unregister a callback."""
107
+ self._callbacks.remove(callback)
62
108
 
63
- def get_js_update(self, value: Any) -> str:
64
- """
65
- Get JavaScript code needed to update this component's value.
109
+ def reenable_callbacks(self) -> None:
110
+ """Reenable all callbacks."""
111
+ pass # Handled by Flask routes and JavaScript
66
112
 
67
- Args:
68
- value: New value to set
113
+ def disable_callbacks(self) -> None:
114
+ """Disable all callbacks."""
115
+ pass # Handled by Flask routes and JavaScript
69
116
 
70
- Returns:
71
- str: JavaScript code that updates the component's value
72
- """
73
- # Default implementation for simple components
74
- return f"document.getElementById('{self.component_id}').value = {self._value_to_js(value)};"
75
-
76
- def _value_to_js(self, value: Any) -> str:
77
- """Convert a Python value to its JavaScript representation."""
78
- if isinstance(value, bool):
79
- return str(value).lower()
80
- elif isinstance(value, (int, float)):
81
- return str(value)
82
- elif isinstance(value, str):
83
- return f"'{value}'"
84
- elif isinstance(value, (list, tuple)):
85
- return f"[{', '.join(self._value_to_js(v) for v in value)}]"
86
- else:
87
- raise ValueError(f"Cannot convert {type(value)} to JavaScript")
88
-
89
-
90
- class TextComponent(BaseWebComponent[TextParameter]):
91
- """Component for text input parameters."""
92
-
93
- def render_html(self) -> str:
94
- return f"""
95
- <div class="form-group">
96
- <label for="{self.component_id}">{self.parameter.name}</label>
97
- <input type="text"
98
- class="form-control"
99
- id="{self.component_id}"
100
- value="{self.parameter.value}">
101
- </div>
102
- """
103
117
 
104
- def get_js_init(self) -> str:
118
+ class TextComponent(BaseComponent[TextParameter]):
119
+ """Component for text parameters."""
120
+
121
+ def _create_html(self, parameter: TextParameter) -> str:
105
122
  return f"""
106
- document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
107
- updateParameter('{self.parameter.name}', e.target.value);
108
- }});
123
+ <div class="{self._style.container_class}">
124
+ <label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
125
+ <input type="text" class="{self._style.input_class}" id="{self._id}"
126
+ name="{parameter.name}" value="{escape(str(parameter.value))}"
127
+ style="width: {self._style.width}; margin: {self._style.margin};"
128
+ data-continuous="{str(self._continuous).lower()}">
129
+ </div>
109
130
  """
110
131
 
111
132
 
112
- class BooleanComponent(BaseWebComponent[BooleanParameter]):
133
+ class BooleanComponent(BaseComponent[BooleanParameter]):
113
134
  """Component for boolean parameters."""
114
135
 
115
- def render_html(self) -> str:
116
- checked = "checked" if self.parameter.value else ""
136
+ def _create_html(self, parameter: BooleanParameter) -> str:
137
+ checked = "checked" if parameter.value else ""
117
138
  return f"""
139
+ <div class="{self._style.container_class}">
118
140
  <div class="form-check">
119
- <input type="checkbox"
120
- class="form-check-input"
121
- id="{self.component_id}"
122
- {checked}>
123
- <label class="form-check-label"
124
- for="{self.component_id}">
125
- {self.parameter.name}
141
+ <input type="checkbox" class="form-check-input" id="{self._id}"
142
+ name="{parameter.name}" {checked}
143
+ style="margin: {self._style.margin};"
144
+ data-continuous="{str(self._continuous).lower()}">
145
+ <label class="form-check-label" for="{self._id}">
146
+ {escape(parameter.name)}
126
147
  </label>
127
148
  </div>
149
+ </div>
128
150
  """
129
151
 
130
- def get_js_init(self) -> str:
131
- return f"""
132
- document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
133
- updateParameter('{self.parameter.name}', e.target.checked);
134
- }});
135
- """
136
-
137
- def get_js_update(self, value: bool) -> str:
138
- return f"document.getElementById('{self.component_id}').checked = {str(value).lower()};"
139
-
140
152
 
141
- class SelectionComponent(BaseWebComponent[SelectionParameter]):
153
+ class SelectionComponent(BaseComponent[SelectionParameter]):
142
154
  """Component for single selection parameters."""
143
155
 
144
- def render_html(self) -> str:
145
- options = []
146
- for opt in self.parameter.options:
147
- selected = "selected" if opt == self.parameter.value else ""
148
- options.append(f'<option value="{opt}" {selected}>{opt}</option>')
156
+ def _create_html(self, parameter: SelectionParameter) -> str:
157
+ options_html = ""
158
+ for option in parameter.options:
159
+ selected = "selected" if option == parameter.value else ""
160
+ options_html += f'<option value="{escape(str(option))}" {selected}>{escape(str(option))}</option>'
149
161
 
150
162
  return f"""
151
- <div class="form-group">
152
- <label for="{self.component_id}">{self.parameter.name}</label>
153
- <select class="form-control" id="{self.component_id}">
154
- {"".join(options)}
155
- </select>
156
- </div>
163
+ <div class="{self._style.container_class}">
164
+ <label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
165
+ <select class="{self._style.input_class}" id="{self._id}"
166
+ name="{parameter.name}"
167
+ style="width: {self._style.width}; margin: {self._style.margin};"
168
+ data-continuous="{str(self._continuous).lower()}">
169
+ {options_html}
170
+ </select>
171
+ </div>
157
172
  """
158
173
 
159
- def get_js_init(self) -> str:
160
- return f"""
161
- document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
162
- updateParameter('{self.parameter.name}', e.target.value);
163
- }});
164
- """
174
+ def matches_parameter(self, parameter: SelectionParameter) -> bool:
175
+ """Check if the component matches the parameter."""
176
+ return self.value == parameter.value and set(self._get_options()) == set(
177
+ parameter.options
178
+ )
165
179
 
180
+ def _get_options(self) -> List[Any]:
181
+ """Get current options from the HTML."""
182
+ # In a real implementation, this would parse the HTML
183
+ # For now, we'll just return an empty list
184
+ return []
166
185
 
167
- class MultipleSelectionComponent(BaseWebComponent[MultipleSelectionParameter]):
168
- """Component for multiple selection parameters."""
186
+ def extra_updates_from_parameter(self, parameter: SelectionParameter) -> None:
187
+ """Extra updates from the parameter."""
188
+ # In a real implementation, this would update the options in the DOM
189
+ pass
169
190
 
170
- def render_html(self) -> str:
171
- options = []
172
- for opt in self.parameter.options:
173
- selected = "selected" if opt in self.parameter.value else ""
174
- options.append(f'<option value="{opt}" {selected}>{opt}</option>')
175
191
 
176
- return f"""
177
- <div class="form-group">
178
- <label for="{self.component_id}">{self.parameter.name}</label>
179
- <select multiple class="form-control" id="{self.component_id}">
180
- {"".join(options)}
181
- </select>
182
- </div>
183
- """
192
+ class MultipleSelectionComponent(BaseComponent[MultipleSelectionParameter]):
193
+ """Component for multiple selection parameters."""
184
194
 
185
- def get_js_init(self) -> str:
186
- return f"""
187
- document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
188
- const selected = Array.from(e.target.selectedOptions).map(opt => opt.value);
189
- updateParameter('{self.parameter.name}', selected);
190
- }});
191
- """
195
+ def _create_html(self, parameter: MultipleSelectionParameter) -> str:
196
+ options_html = ""
197
+ for option in parameter.options:
198
+ selected = "selected" if option in parameter.value else ""
199
+ options_html += f'<option value="{escape(str(option))}" {selected}>{escape(str(option))}</option>'
192
200
 
193
- def get_js_update(self, values: List[str]) -> str:
194
- # More complex update for multi-select
195
201
  return f"""
196
- const sel = document.getElementById('{self.component_id}');
197
- Array.from(sel.options).forEach(opt => {{
198
- opt.selected = {self._value_to_js(values)}.includes(opt.value);
199
- }});
202
+ <div class="{self._style.container_class}">
203
+ <label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
204
+ <select class="{self._style.input_class}" id="{self._id}"
205
+ name="{parameter.name}" multiple
206
+ style="width: {self._style.width}; margin: {self._style.margin};"
207
+ data-continuous="{str(self._continuous).lower()}">
208
+ {options_html}
209
+ </select>
210
+ </div>
200
211
  """
201
212
 
213
+ def matches_parameter(self, parameter: MultipleSelectionParameter) -> bool:
214
+ """Check if the component matches the parameter."""
215
+ return set(self.value) == set(parameter.value) and set(
216
+ self._get_options()
217
+ ) == set(parameter.options)
218
+
219
+ def _get_options(self) -> List[Any]:
220
+ """Get current options from the HTML."""
221
+ # In a real implementation, this would parse the HTML
222
+ return []
223
+
224
+ def extra_updates_from_parameter(
225
+ self, parameter: MultipleSelectionParameter
226
+ ) -> None:
227
+ """Extra updates from the parameter."""
228
+ # In a real implementation, this would update the options in the DOM
229
+ pass
230
+
202
231
 
203
- class SliderMixin:
204
- """Shared functionality for slider components."""
232
+ class IntegerComponent(BaseComponent[IntegerParameter]):
233
+ """Component for integer parameters."""
205
234
 
206
- def _get_slider_js_init(self, component_id: str, param_name: str) -> str:
235
+ def _create_html(self, parameter: IntegerParameter) -> str:
207
236
  return f"""
208
- noUiSlider.create(document.getElementById('{component_id}'), {{
209
- start: {self._value_to_js(self.parameter.value)},
210
- connect: true,
211
- range: {{
212
- 'min': {self.parameter.min_value},
213
- 'max': {self.parameter.max_value}
214
- }}
215
- }}).on('change', (values) => {{
216
- updateParameter('{param_name}', parseFloat(values[0]));
217
- const value = parseFloat(values[0]);
218
- // Update the display text
219
- document.getElementById('{component_id}_display').textContent = value.toFixed(2);
220
- debouncedUpdateParameter('{param_name}', value);
221
- }});
237
+ <div class="{self._style.container_class}">
238
+ <label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
239
+ <input type="range" class="{self._style.input_class}" id="{self._id}"
240
+ name="{parameter.name}" value="{parameter.value}"
241
+ min="{parameter.min_value}" max="{parameter.max_value}"
242
+ style="width: {self._style.width}; margin: {self._style.margin};"
243
+ data-continuous="{str(self._continuous).lower()}">
244
+ <output for="{self._id}">{parameter.value}</output>
245
+ </div>
222
246
  """
223
247
 
248
+ def matches_parameter(self, parameter: IntegerParameter) -> bool:
249
+ """Check if the component matches the parameter."""
250
+ return (
251
+ self.value == parameter.value
252
+ and self._get_min() == parameter.min_value
253
+ and self._get_max() == parameter.max_value
254
+ )
224
255
 
225
- class IntegerComponent(SliderMixin, BaseWebComponent[IntegerParameter]):
226
- """Component for integer parameters."""
256
+ def _get_min(self) -> int:
257
+ """Get minimum value from the HTML."""
258
+ return 0 # Placeholder
227
259
 
228
- def render_html(self) -> str:
229
- return f"""
230
- <div class="form-group">
231
- <label for="{self.component_id}">{self.parameter.name}</label>
232
- <div id="{self.component_id}" class="slider"></div>
233
- <span id="{self.component_id}_display" class="value-display">{self.parameter.value}</span>
234
- </div>
235
- """
260
+ def _get_max(self) -> int:
261
+ """Get maximum value from the HTML."""
262
+ return 100 # Placeholder
236
263
 
237
- def get_js_init(self) -> str:
238
- return self._get_slider_js_init(self.component_id, self.parameter.name)
264
+ def extra_updates_from_parameter(self, parameter: IntegerParameter) -> None:
265
+ """Extra updates from the parameter."""
266
+ # In a real implementation, this would update min/max in the DOM
267
+ pass
239
268
 
240
269
 
241
- class FloatComponent(SliderMixin, BaseWebComponent[FloatParameter]):
270
+ class FloatComponent(BaseComponent[FloatParameter]):
242
271
  """Component for float parameters."""
243
272
 
244
- def render_html(self) -> str:
273
+ def _create_html(self, parameter: FloatParameter) -> str:
245
274
  return f"""
246
- <div class="form-group">
247
- <label for="{self.component_id}">{self.parameter.name}</label>
248
- <div id="{self.component_id}" class="slider"></div>
249
- <span id="{self.component_id}_display" class="value-display">{self.parameter.value:.2f}</span>
250
- </div>
275
+ <div class="{self._style.container_class}">
276
+ <label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
277
+ <input type="range" class="{self._style.input_class}" id="{self._id}"
278
+ name="{parameter.name}" value="{parameter.value}"
279
+ min="{parameter.min_value}" max="{parameter.max_value}" step="{parameter.step}"
280
+ style="width: {self._style.width}; margin: {self._style.margin};"
281
+ data-continuous="{str(self._continuous).lower()}">
282
+ <output for="{self._id}">{parameter.value}</output>
283
+ </div>
251
284
  """
252
285
 
253
- def get_js_init(self) -> str:
254
- return self._get_slider_js_init(self.component_id, self.parameter.name)
286
+ def matches_parameter(self, parameter: FloatParameter) -> bool:
287
+ """Check if the component matches the parameter."""
288
+ return (
289
+ self.value == parameter.value
290
+ and self._get_min() == parameter.min_value
291
+ and self._get_max() == parameter.max_value
292
+ and self._get_step() == parameter.step
293
+ )
255
294
 
295
+ def _get_min(self) -> float:
296
+ """Get minimum value from the HTML."""
297
+ return 0.0 # Placeholder
256
298
 
257
- class RangeSliderMixin:
258
- """Shared functionality for range slider components."""
299
+ def _get_max(self) -> float:
300
+ """Get maximum value from the HTML."""
301
+ return 1.0 # Placeholder
259
302
 
260
- def _get_range_slider_js_init(self, component_id: str, param_name: str) -> str:
261
- return f"""
262
- noUiSlider.create(document.getElementById('{component_id}'), {{
263
- start: {self._value_to_js(self.parameter.value)},
264
- connect: true,
265
- range: {{
266
- 'min': {self.parameter.min_value},
267
- 'max': {self.parameter.max_value}
268
- }}
269
- }}).on('change', (values) => {{
270
- updateParameter('{param_name}', [
271
- parseFloat(values[0]),
272
- parseFloat(values[1])
273
- ]);
274
- }});
275
- """
303
+ def _get_step(self) -> float:
304
+ """Get step value from the HTML."""
305
+ return 0.1 # Placeholder
306
+
307
+ def extra_updates_from_parameter(self, parameter: FloatParameter) -> None:
308
+ """Extra updates from the parameter."""
309
+ # In a real implementation, this would update min/max/step in the DOM
310
+ pass
276
311
 
277
312
 
278
- class IntegerRangeComponent(RangeSliderMixin, BaseWebComponent[IntegerRangeParameter]):
313
+ class IntegerRangeComponent(BaseComponent[IntegerRangeParameter]):
279
314
  """Component for integer range parameters."""
280
315
 
281
- def render_html(self) -> str:
282
- low, high = self.parameter.value
316
+ def _create_html(self, parameter: IntegerRangeParameter) -> str:
317
+ low, high = parameter.value
283
318
  return f"""
284
- <div class="form-group">
285
- <label for="{self.component_id}">{self.parameter.name}</label>
286
- <div id="{self.component_id}" class="range-slider"></div>
287
- <span class="value-display">{low} - {high}</span>
319
+ <div class="{self._style.container_class}">
320
+ <label for="{self._id}_low" class="{self._style.label_class}">{escape(parameter.name)}</label>
321
+ <div class="d-flex align-items-center">
322
+ <input type="range" class="{self._style.input_class}" id="{self._id}_low"
323
+ name="{parameter.name}_low" value="{low}"
324
+ min="{parameter.min_value}" max="{parameter.max_value}"
325
+ style="width: {self._style.width}; margin: {self._style.margin};"
326
+ data-continuous="{str(self._continuous).lower()}">
327
+ <output for="{self._id}_low">{low}</output>
328
+ </div>
329
+ <div class="d-flex align-items-center">
330
+ <input type="range" class="{self._style.input_class}" id="{self._id}_high"
331
+ name="{parameter.name}_high" value="{high}"
332
+ min="{parameter.min_value}" max="{parameter.max_value}"
333
+ style="width: {self._style.width}; margin: {self._style.margin};"
334
+ data-continuous="{str(self._continuous).lower()}">
335
+ <output for="{self._id}_high">{high}</output>
288
336
  </div>
337
+ </div>
289
338
  """
290
339
 
291
- def get_js_init(self) -> str:
292
- return self._get_range_slider_js_init(self.component_id, self.parameter.name)
340
+ def matches_parameter(self, parameter: IntegerRangeParameter) -> bool:
341
+ """Check if the component matches the parameter."""
342
+ return (
343
+ self.value == parameter.value
344
+ and self._get_min() == parameter.min_value
345
+ and self._get_max() == parameter.max_value
346
+ )
347
+
348
+ def _get_min(self) -> int:
349
+ """Get minimum value from the HTML."""
350
+ return 0 # Placeholder
351
+
352
+ def _get_max(self) -> int:
353
+ """Get maximum value from the HTML."""
354
+ return 100 # Placeholder
355
+
356
+ def extra_updates_from_parameter(self, parameter: IntegerRangeParameter) -> None:
357
+ """Extra updates from the parameter."""
358
+ # In a real implementation, this would update min/max in the DOM
359
+ pass
293
360
 
294
361
 
295
- class FloatRangeComponent(RangeSliderMixin, BaseWebComponent[FloatRangeParameter]):
362
+ class FloatRangeComponent(BaseComponent[FloatRangeParameter]):
296
363
  """Component for float range parameters."""
297
364
 
298
- def render_html(self) -> str:
299
- low, high = self.parameter.value
365
+ def _create_html(self, parameter: FloatRangeParameter) -> str:
366
+ low, high = parameter.value
300
367
  return f"""
301
- <div class="form-group">
302
- <label for="{self.component_id}">{self.parameter.name}</label>
303
- <div id="{self.component_id}" class="range-slider"></div>
304
- <span class="value-display">{low:.2f} - {high:.2f}</span>
368
+ <div class="{self._style.container_class}">
369
+ <label for="{self._id}_low" class="{self._style.label_class}">{escape(parameter.name)}</label>
370
+ <div class="d-flex align-items-center">
371
+ <input type="range" class="{self._style.input_class}" id="{self._id}_low"
372
+ name="{parameter.name}_low" value="{low}"
373
+ min="{parameter.min_value}" max="{parameter.max_value}" step="{parameter.step}"
374
+ style="width: {self._style.width}; margin: {self._style.margin};"
375
+ data-continuous="{str(self._continuous).lower()}">
376
+ <output for="{self._id}_low">{low}</output>
377
+ </div>
378
+ <div class="d-flex align-items-center">
379
+ <input type="range" class="{self._style.input_class}" id="{self._id}_high"
380
+ name="{parameter.name}_high" value="{high}"
381
+ min="{parameter.min_value}" max="{parameter.max_value}" step="{parameter.step}"
382
+ style="width: {self._style.width}; margin: {self._style.margin};"
383
+ data-continuous="{str(self._continuous).lower()}">
384
+ <output for="{self._id}_high">{high}</output>
305
385
  </div>
386
+ </div>
306
387
  """
307
388
 
308
- def get_js_init(self) -> str:
309
- return self._get_range_slider_js_init(self.component_id, self.parameter.name)
389
+ def matches_parameter(self, parameter: FloatRangeParameter) -> bool:
390
+ """Check if the component matches the parameter."""
391
+ return (
392
+ self.value == parameter.value
393
+ and self._get_min() == parameter.min_value
394
+ and self._get_max() == parameter.max_value
395
+ and self._get_step() == parameter.step
396
+ )
310
397
 
398
+ def _get_min(self) -> float:
399
+ """Get minimum value from the HTML."""
400
+ return 0.0 # Placeholder
311
401
 
312
- class UnboundedIntegerComponent(BaseWebComponent[UnboundedIntegerParameter]):
313
- """Component for unbounded integer parameters."""
402
+ def _get_max(self) -> float:
403
+ """Get maximum value from the HTML."""
404
+ return 1.0 # Placeholder
314
405
 
315
- def render_html(self) -> str:
316
- min_attr = (
317
- f'min="{self.parameter.min_value}"'
318
- if self.parameter.min_value is not None
319
- else ""
320
- )
321
- max_attr = (
322
- f'max="{self.parameter.max_value}"'
323
- if self.parameter.max_value is not None
324
- else ""
325
- )
406
+ def _get_step(self) -> float:
407
+ """Get step value from the HTML."""
408
+ return 0.1 # Placeholder
409
+
410
+ def extra_updates_from_parameter(self, parameter: FloatRangeParameter) -> None:
411
+ """Extra updates from the parameter."""
412
+ # In a real implementation, this would update min/max/step in the DOM
413
+ pass
326
414
 
327
- return f"""
328
- <div class="form-group">
329
- <label for="{self.component_id}">{self.parameter.name}</label>
330
- <input type="number"
331
- class="form-control"
332
- id="{self.component_id}"
333
- value="{self.parameter.value}"
334
- {min_attr}
335
- {max_attr}
336
- step="1">
337
- </div>
338
- """
339
415
 
340
- def get_js_init(self) -> str:
416
+ class UnboundedIntegerComponent(BaseComponent[UnboundedIntegerParameter]):
417
+ """Component for unbounded integer parameters."""
418
+
419
+ def _create_html(self, parameter: UnboundedIntegerParameter) -> str:
341
420
  return f"""
342
- document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
343
- updateParameter('{self.parameter.name}', parseInt(e.target.value));
344
- }});
421
+ <div class="{self._style.container_class}">
422
+ <label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
423
+ <input type="number" class="{self._style.input_class}" id="{self._id}"
424
+ name="{parameter.name}" value="{parameter.value}"
425
+ style="width: {self._style.width}; margin: {self._style.margin};"
426
+ data-continuous="{str(self._continuous).lower()}">
427
+ </div>
345
428
  """
346
429
 
347
430
 
348
- class UnboundedFloatComponent(BaseWebComponent[UnboundedFloatParameter]):
431
+ class UnboundedFloatComponent(BaseComponent[UnboundedFloatParameter]):
349
432
  """Component for unbounded float parameters."""
350
433
 
351
- def render_html(self) -> str:
352
- min_attr = (
353
- f'min="{self.parameter.min_value}"'
354
- if self.parameter.min_value is not None
355
- else ""
356
- )
357
- max_attr = (
358
- f'max="{self.parameter.max_value}"'
359
- if self.parameter.max_value is not None
360
- else ""
361
- )
362
- step_attr = (
363
- f'step="{self.parameter.step}"' if self.parameter.step is not None else ""
364
- )
365
-
434
+ def _create_html(self, parameter: UnboundedFloatParameter) -> str:
435
+ step = parameter.step if parameter.step is not None else "any"
366
436
  return f"""
367
- <div class="form-group">
368
- <label for="{self.component_id}">{self.parameter.name}</label>
369
- <input type="number"
370
- class="form-control"
371
- id="{self.component_id}"
372
- value="{self.parameter.value}"
373
- {min_attr}
374
- {max_attr}
375
- {step_attr}>
376
- </div>
437
+ <div class="{self._style.container_class}">
438
+ <label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
439
+ <input type="number" class="{self._style.input_class}" id="{self._id}"
440
+ name="{parameter.name}" value="{parameter.value}" step="{step}"
441
+ style="width: {self._style.width}; margin: {self._style.margin};"
442
+ data-continuous="{str(self._continuous).lower()}">
443
+ </div>
377
444
  """
378
445
 
379
- def get_js_init(self) -> str:
380
- return f"""
381
- document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
382
- updateParameter('{self.parameter.name}', parseFloat(e.target.value));
383
- }});
384
- """
446
+ def matches_parameter(self, parameter: UnboundedFloatParameter) -> bool:
447
+ """Check if the component matches the parameter."""
448
+ return self.value == parameter.value and self._get_step() == parameter.step
385
449
 
450
+ def _get_step(self) -> Optional[float]:
451
+ """Get step value from the HTML."""
452
+ return None # Placeholder
386
453
 
387
- class ButtonComponent(BaseWebComponent[ButtonParameter]):
454
+ def extra_updates_from_parameter(self, parameter: UnboundedFloatParameter) -> None:
455
+ """Extra updates from the parameter."""
456
+ # In a real implementation, this would update step in the DOM
457
+ pass
458
+
459
+
460
+ class ButtonComponent(BaseComponent[ButtonAction]):
388
461
  """Component for button parameters."""
389
462
 
390
- def render_html(self) -> str:
391
- return f"""
392
- <div class="form-group">
393
- <button type="button"
394
- class="btn btn-primary"
395
- id="{self.component_id}">
396
- {self.parameter.label}
397
- </button>
398
- </div>
399
- """
463
+ _is_action: bool = True
400
464
 
401
- def get_js_init(self) -> str:
465
+ def _create_html(self, parameter: ButtonAction) -> str:
402
466
  return f"""
403
- document.getElementById('{self.component_id}').addEventListener('click', () => {{
404
- buttonClick('{self.parameter.name}');
405
- }});
467
+ <div class="{self._style.container_class}">
468
+ <button type="button" class="btn btn-primary" id="{self._id}"
469
+ name="{parameter.name}"
470
+ style="width: {self._style.width}; margin: {self._style.margin};">
471
+ {escape(parameter.label)}
472
+ </button>
473
+ </div>
406
474
  """
407
475
 
476
+ def matches_parameter(self, parameter: ButtonAction) -> bool:
477
+ """Check if the component matches the parameter."""
478
+ return True # Buttons don't have a value to match
479
+
480
+ def extra_updates_from_parameter(self, parameter: ButtonAction) -> None:
481
+ """Extra updates from the parameter."""
482
+ # In a real implementation, this would update the button label in the DOM
483
+ pass
408
484
 
409
- def create_web_component(
410
- parameter: Parameter[Any], component_id: str
411
- ) -> BaseWebComponent[Parameter[Any]]:
412
- """Create and return the appropriate web component for the given parameter."""
413
485
 
486
+ def create_component(
487
+ parameter: Union[Parameter[Any], ButtonAction],
488
+ continuous: bool = False,
489
+ style: Optional[ComponentStyle] = None,
490
+ ) -> BaseComponent[Union[Parameter[Any], ButtonAction]]:
491
+ """Create the appropriate component for a parameter."""
414
492
  component_map = {
415
493
  TextParameter: TextComponent,
416
494
  BooleanParameter: BooleanComponent,
@@ -422,76 +500,11 @@ def create_web_component(
422
500
  FloatRangeParameter: FloatRangeComponent,
423
501
  UnboundedIntegerParameter: UnboundedIntegerComponent,
424
502
  UnboundedFloatParameter: UnboundedFloatComponent,
425
- ButtonParameter: ButtonComponent,
503
+ ButtonAction: ButtonComponent,
426
504
  }
427
505
 
428
- component_class = component_map.get(type(parameter))
429
- if component_class is None:
430
- raise ValueError(
431
- f"No component implementation for parameter type: {type(parameter)}"
432
- )
433
-
434
- return component_class(parameter, component_id)
435
-
506
+ for param_type, component_class in component_map.items():
507
+ if isinstance(parameter, param_type):
508
+ return component_class(parameter, continuous, style)
436
509
 
437
- class WebComponentCollection:
438
- """
439
- Manages a collection of web components for a viewer's parameters.
440
-
441
- This class helps organize all the components needed for a viewer,
442
- handling their creation, initialization, and updates.
443
- """
444
-
445
- def __init__(self):
446
- self.components: Dict[str, BaseWebComponent] = {}
447
-
448
- def add_component(self, name: str, parameter: Parameter[Any]) -> None:
449
- """Add a new component for the given parameter."""
450
- component_id = f"param_{name}"
451
- self.components[name] = create_web_component(parameter, component_id)
452
-
453
- def get_all_html(self) -> str:
454
- """Get the combined HTML for all components."""
455
- return "\n".join(comp.render_html() for comp in self.components.values())
456
-
457
- def get_init_js(self) -> str:
458
- """Get the combined initialization JavaScript for all components."""
459
- return "\n".join(comp.get_js_init() for comp in self.components.values())
460
-
461
- def get_update_js(self, name: str, value: Any) -> str:
462
- """Get the JavaScript to update a specific component's value."""
463
- if name not in self.components:
464
- raise ValueError(f"No component found for parameter: {name}")
465
- return self.components[name].get_js_update(value)
466
-
467
- def get_required_css(self) -> List[str]:
468
- """Get list of CSS files required by the components."""
469
- return [
470
- "https://cdn.jsdelivr.net/npm/nouislider@14.6.3/distribute/nouislider.min.css",
471
- "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css",
472
- ]
473
-
474
- def get_required_js(self) -> List[str]:
475
- """Get list of JavaScript files required by the components."""
476
- return [
477
- "https://cdn.jsdelivr.net/npm/nouislider@14.6.3/distribute/nouislider.min.js",
478
- "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js",
479
- ]
480
-
481
- def get_custom_styles(self) -> str:
482
- """Get custom CSS styles needed for the components."""
483
- return """
484
- .slider, .range-slider {
485
- margin: 10px 0;
486
- }
487
- .value-display {
488
- display: block;
489
- text-align: center;
490
- margin-top: 5px;
491
- font-size: 0.9em;
492
- color: #666;
493
- }
494
- .form-group {
495
- margin-bottom: 1rem;
496
- }
497
- """
510
+ raise ValueError(f"No component available for parameter type: {type(parameter)}")