streamlit-sortable-multiselect 0.7.3__tar.gz → 0.7.5__tar.gz

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 (18) hide show
  1. {streamlit_sortable_multiselect-0.7.3/streamlit_sortable_multiselect.egg-info → streamlit_sortable_multiselect-0.7.5}/PKG-INFO +88 -1
  2. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/README.md +87 -0
  3. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/pyproject.toml +1 -1
  4. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect/__init__.py +145 -7
  5. streamlit_sortable_multiselect-0.7.5/streamlit_sortable_multiselect/frontend/build/assets/index-DGpJ50qI.css +1 -0
  6. streamlit_sortable_multiselect-0.7.5/streamlit_sortable_multiselect/frontend/build/assets/index-RH2LvVA2.js +67 -0
  7. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect/frontend/build/index.html +2 -2
  8. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5/streamlit_sortable_multiselect.egg-info}/PKG-INFO +88 -1
  9. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect.egg-info/SOURCES.txt +2 -2
  10. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/tests/test_wrapper.py +64 -0
  11. streamlit_sortable_multiselect-0.7.3/streamlit_sortable_multiselect/frontend/build/assets/index-DdljhZDS.js +0 -67
  12. streamlit_sortable_multiselect-0.7.3/streamlit_sortable_multiselect/frontend/build/assets/index-DobBL0Fg.css +0 -1
  13. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/LICENSE +0 -0
  14. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/MANIFEST.in +0 -0
  15. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/setup.cfg +0 -0
  16. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect.egg-info/dependency_links.txt +0 -0
  17. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect.egg-info/requires.txt +0 -0
  18. {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamlit-sortable-multiselect
3
- Version: 0.7.3
3
+ Version: 0.7.5
4
4
  Summary: A Streamlit custom component for selecting and reordering multiple string values.
5
5
  Author: Kentaro Aoki
6
6
  License-Expression: MIT
@@ -36,6 +36,12 @@ A Streamlit custom component for searching, selecting, and reordering multiple s
36
36
  python -m pip install streamlit-sortable-multiselect
37
37
  ```
38
38
 
39
+ Upgrade an existing installation:
40
+
41
+ ```bash
42
+ python -m pip install --upgrade streamlit-sortable-multiselect
43
+ ```
44
+
39
45
  For local development:
40
46
 
41
47
  ```bash
@@ -104,6 +110,17 @@ st.write(selected)
104
110
  | `selected_position` | `str` | `"bottom"` | Position of the selected item list relative to the search/add input. Use `"bottom"` or `"top"`. |
105
111
  | `icon_size` | `int` | `20` | Icon display size in pixels for `icon_url` images. Images are displayed inside a square area while preserving their aspect ratio. |
106
112
  | `options_max_height` | `int` | `190` | Maximum height in pixels for the available options dropdown. |
113
+ | `suggestions_api_url` | `str \| None` | `None` | Absolute HTTP(S) endpoint used to fetch suggestions in the browser. `None` disables API suggestions. |
114
+ | `suggestions_query_param` | `str` | `"q"` | Query parameter name used to send the current search text. |
115
+ | `suggestions_response_path` | `str` | `""` | Dot-separated path to the suggestions array in the JSON response. Empty means the response root. |
116
+ | `suggestions_label_path` | `str` | `"label"` | Dot-separated path to each suggestion's display label. |
117
+ | `suggestions_value_path` | `str` | `"value"` | Dot-separated path to each suggestion's returned value. |
118
+ | `suggestions_icon_url_path` | `str \| None` | `"icon_url"` | Optional dot-separated path to each suggestion's icon URL. `None` disables API icons. |
119
+ | `suggestions_headers` | `Mapping[str, str] \| None` | `None` | HTTP headers sent with suggestions requests. Header values are visible to browser users and must not contain secrets. |
120
+ | `suggestions_min_chars` | `int` | `1` | Minimum trimmed query length before requesting suggestions. Use `0` to allow an empty query. |
121
+ | `suggestions_debounce_ms` | `int` | `300` | Delay in milliseconds between the latest input and the API request. |
122
+ | `suggestions_loading_message` | `str` | `"Loading suggestions..."` | Message shown while an API request is in progress. |
123
+ | `suggestions_error_message` | `str` | `"Failed to load suggestions"` | Message shown when the request or response cannot be processed. |
107
124
  | `key` | `str \| None` | `None` | Optional Streamlit component key. Use this when rendering multiple sortable multiselects. |
108
125
 
109
126
  Option dictionaries use this shape:
@@ -114,6 +131,54 @@ Option dictionaries use this shape:
114
131
 
115
132
  `icon_url` may be omitted. The returned value is always the `value`, not the display `label`.
116
133
 
134
+ ## API Suggestions
135
+
136
+ Set `suggestions_api_url` to request suggestions as the user types. Static `options` that match the query are shown first, followed by API results. Duplicate `value` entries prefer the static option.
137
+
138
+ ```python
139
+ selected = sortable_multiselect(
140
+ "Repositories",
141
+ options=[{"label": "Streamlit", "value": "streamlit/streamlit"}],
142
+ suggestions_api_url="https://api.example.com/repositories",
143
+ suggestions_query_param="query",
144
+ suggestions_response_path="data.items",
145
+ suggestions_label_path="name",
146
+ suggestions_value_path="full_name",
147
+ suggestions_icon_url_path="owner.avatar_url",
148
+ suggestions_headers={"X-Public-Client": "streamlit-app"},
149
+ suggestions_min_chars=2,
150
+ suggestions_debounce_ms=300,
151
+ suggestions_loading_message="Searching repositories...",
152
+ suggestions_error_message="Repository search is unavailable",
153
+ )
154
+ ```
155
+
156
+ The example above accepts a response such as:
157
+
158
+ ```json
159
+ {
160
+ "data": {
161
+ "items": [
162
+ {
163
+ "name": "streamlit-sortable-multiselect",
164
+ "full_name": "example/streamlit-sortable-multiselect",
165
+ "owner": {
166
+ "avatar_url": "https://example.com/avatar.png"
167
+ }
168
+ }
169
+ ]
170
+ }
171
+ }
172
+ ```
173
+
174
+ Requests are made directly from the component iframe, so the endpoint must allow browser requests with CORS. Values in `suggestions_headers` are exposed to browser users. Do not pass API secrets, private bearer tokens, or other credentials. Use a server-side proxy when authentication must remain private.
175
+
176
+ The component sends a `GET` request after the trimmed input reaches
177
+ `suggestions_min_chars` and remains unchanged for `suggestions_debounce_ms`.
178
+ Changing the input cancels the previous in-flight request. Set `options=[]` to
179
+ use only API results. Selected API values remain selected when a later query
180
+ returns different suggestions.
181
+
117
182
  Build the frontend before packaging or using release mode:
118
183
 
119
184
  ```bash
@@ -125,8 +190,30 @@ Run the example app:
125
190
 
126
191
  ```bash
127
192
  streamlit run examples/basic.py
193
+ streamlit run examples/api_suggestions.py
194
+ ```
195
+
196
+ `api_suggestions.py` starts a CORS-enabled sample API on a local random port, so
197
+ it can be tried without a separate API process. This embedded server is intended
198
+ for local development; use a separately deployed HTTPS API in remote deployments.
199
+ The Streamlit page displays the address and port selected for the API. Test the
200
+ endpoint from the same machine with:
201
+
202
+ ```bash
203
+ curl "http://127.0.0.1:<displayed-port>/suggest?q=py"
204
+ ```
205
+
206
+ Pass a fixed API port after `--` when needed:
207
+
208
+ ```bash
209
+ streamlit run examples/api_suggestions.py -- --api-port 8765
128
210
  ```
129
211
 
212
+ The embedded API binds to `127.0.0.1`. Because suggestions are fetched by the
213
+ browser, it works only when the browser and Streamlit run on the same machine.
214
+ For Docker, another computer, or a hosted Streamlit app, configure
215
+ `suggestions_api_url` with a browser-accessible HTTPS endpoint instead.
216
+
130
217
  ## Release
131
218
 
132
219
  Build and check the distribution files:
@@ -8,6 +8,12 @@ A Streamlit custom component for searching, selecting, and reordering multiple s
8
8
  python -m pip install streamlit-sortable-multiselect
9
9
  ```
10
10
 
11
+ Upgrade an existing installation:
12
+
13
+ ```bash
14
+ python -m pip install --upgrade streamlit-sortable-multiselect
15
+ ```
16
+
11
17
  For local development:
12
18
 
13
19
  ```bash
@@ -76,6 +82,17 @@ st.write(selected)
76
82
  | `selected_position` | `str` | `"bottom"` | Position of the selected item list relative to the search/add input. Use `"bottom"` or `"top"`. |
77
83
  | `icon_size` | `int` | `20` | Icon display size in pixels for `icon_url` images. Images are displayed inside a square area while preserving their aspect ratio. |
78
84
  | `options_max_height` | `int` | `190` | Maximum height in pixels for the available options dropdown. |
85
+ | `suggestions_api_url` | `str \| None` | `None` | Absolute HTTP(S) endpoint used to fetch suggestions in the browser. `None` disables API suggestions. |
86
+ | `suggestions_query_param` | `str` | `"q"` | Query parameter name used to send the current search text. |
87
+ | `suggestions_response_path` | `str` | `""` | Dot-separated path to the suggestions array in the JSON response. Empty means the response root. |
88
+ | `suggestions_label_path` | `str` | `"label"` | Dot-separated path to each suggestion's display label. |
89
+ | `suggestions_value_path` | `str` | `"value"` | Dot-separated path to each suggestion's returned value. |
90
+ | `suggestions_icon_url_path` | `str \| None` | `"icon_url"` | Optional dot-separated path to each suggestion's icon URL. `None` disables API icons. |
91
+ | `suggestions_headers` | `Mapping[str, str] \| None` | `None` | HTTP headers sent with suggestions requests. Header values are visible to browser users and must not contain secrets. |
92
+ | `suggestions_min_chars` | `int` | `1` | Minimum trimmed query length before requesting suggestions. Use `0` to allow an empty query. |
93
+ | `suggestions_debounce_ms` | `int` | `300` | Delay in milliseconds between the latest input and the API request. |
94
+ | `suggestions_loading_message` | `str` | `"Loading suggestions..."` | Message shown while an API request is in progress. |
95
+ | `suggestions_error_message` | `str` | `"Failed to load suggestions"` | Message shown when the request or response cannot be processed. |
79
96
  | `key` | `str \| None` | `None` | Optional Streamlit component key. Use this when rendering multiple sortable multiselects. |
80
97
 
81
98
  Option dictionaries use this shape:
@@ -86,6 +103,54 @@ Option dictionaries use this shape:
86
103
 
87
104
  `icon_url` may be omitted. The returned value is always the `value`, not the display `label`.
88
105
 
106
+ ## API Suggestions
107
+
108
+ Set `suggestions_api_url` to request suggestions as the user types. Static `options` that match the query are shown first, followed by API results. Duplicate `value` entries prefer the static option.
109
+
110
+ ```python
111
+ selected = sortable_multiselect(
112
+ "Repositories",
113
+ options=[{"label": "Streamlit", "value": "streamlit/streamlit"}],
114
+ suggestions_api_url="https://api.example.com/repositories",
115
+ suggestions_query_param="query",
116
+ suggestions_response_path="data.items",
117
+ suggestions_label_path="name",
118
+ suggestions_value_path="full_name",
119
+ suggestions_icon_url_path="owner.avatar_url",
120
+ suggestions_headers={"X-Public-Client": "streamlit-app"},
121
+ suggestions_min_chars=2,
122
+ suggestions_debounce_ms=300,
123
+ suggestions_loading_message="Searching repositories...",
124
+ suggestions_error_message="Repository search is unavailable",
125
+ )
126
+ ```
127
+
128
+ The example above accepts a response such as:
129
+
130
+ ```json
131
+ {
132
+ "data": {
133
+ "items": [
134
+ {
135
+ "name": "streamlit-sortable-multiselect",
136
+ "full_name": "example/streamlit-sortable-multiselect",
137
+ "owner": {
138
+ "avatar_url": "https://example.com/avatar.png"
139
+ }
140
+ }
141
+ ]
142
+ }
143
+ }
144
+ ```
145
+
146
+ Requests are made directly from the component iframe, so the endpoint must allow browser requests with CORS. Values in `suggestions_headers` are exposed to browser users. Do not pass API secrets, private bearer tokens, or other credentials. Use a server-side proxy when authentication must remain private.
147
+
148
+ The component sends a `GET` request after the trimmed input reaches
149
+ `suggestions_min_chars` and remains unchanged for `suggestions_debounce_ms`.
150
+ Changing the input cancels the previous in-flight request. Set `options=[]` to
151
+ use only API results. Selected API values remain selected when a later query
152
+ returns different suggestions.
153
+
89
154
  Build the frontend before packaging or using release mode:
90
155
 
91
156
  ```bash
@@ -97,8 +162,30 @@ Run the example app:
97
162
 
98
163
  ```bash
99
164
  streamlit run examples/basic.py
165
+ streamlit run examples/api_suggestions.py
166
+ ```
167
+
168
+ `api_suggestions.py` starts a CORS-enabled sample API on a local random port, so
169
+ it can be tried without a separate API process. This embedded server is intended
170
+ for local development; use a separately deployed HTTPS API in remote deployments.
171
+ The Streamlit page displays the address and port selected for the API. Test the
172
+ endpoint from the same machine with:
173
+
174
+ ```bash
175
+ curl "http://127.0.0.1:<displayed-port>/suggest?q=py"
176
+ ```
177
+
178
+ Pass a fixed API port after `--` when needed:
179
+
180
+ ```bash
181
+ streamlit run examples/api_suggestions.py -- --api-port 8765
100
182
  ```
101
183
 
184
+ The embedded API binds to `127.0.0.1`. Because suggestions are fetched by the
185
+ browser, it works only when the browser and Streamlit run on the same machine.
186
+ For Docker, another computer, or a hosted Streamlit app, configure
187
+ `suggestions_api_url` with a browser-accessible HTTPS endpoint instead.
188
+
102
189
  ## Release
103
190
 
104
191
  Build and check the distribution files:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "streamlit-sortable-multiselect"
7
- version = "0.7.3"
7
+ version = "0.7.5"
8
8
  description = "A Streamlit custom component for selecting and reordering multiple string values."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -5,10 +5,11 @@ from __future__ import annotations
5
5
  import os
6
6
  from pathlib import Path
7
7
  from typing import Any, Iterable, Mapping, Sequence, cast
8
+ from urllib.parse import urlparse
8
9
 
9
10
  import streamlit.components.v1 as components
10
11
 
11
- __version__ = "0.7.3"
12
+ __version__ = "0.7.5"
12
13
  __all__ = ["sortable_multiselect"]
13
14
 
14
15
  _COMPONENT_NAME = "streamlit_sortable_multiselect"
@@ -111,6 +112,59 @@ def _validate_options_max_height(options_max_height: int) -> int:
111
112
  return options_max_height
112
113
 
113
114
 
115
+ def _validate_non_negative_int(name: str, value: int) -> int:
116
+ if isinstance(value, bool) or not isinstance(value, int):
117
+ raise TypeError(f"{name} must be an integer.")
118
+ if value < 0:
119
+ raise ValueError(f"{name} must be 0 or greater.")
120
+ return value
121
+
122
+
123
+ def _validate_optional_non_empty_string(name: str, value: str | None) -> str | None:
124
+ if value is None:
125
+ return None
126
+ if not isinstance(value, str):
127
+ raise TypeError(f"{name} must be a string or None.")
128
+ if not value:
129
+ raise ValueError(f"{name} must not be empty.")
130
+ return value
131
+
132
+
133
+ def _validate_non_empty_string(name: str, value: str) -> str:
134
+ if not isinstance(value, str):
135
+ raise TypeError(f"{name} must be a string.")
136
+ if not value:
137
+ raise ValueError(f"{name} must not be empty.")
138
+ return value
139
+
140
+
141
+ def _validate_suggestions_api_url(value: str | None) -> str | None:
142
+ validated = _validate_optional_non_empty_string("suggestions_api_url", value)
143
+ if validated is None:
144
+ return None
145
+
146
+ parsed = urlparse(validated)
147
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
148
+ raise ValueError("suggestions_api_url must be an absolute HTTP(S) URL.")
149
+ return validated
150
+
151
+
152
+ def _normalize_suggestions_headers(
153
+ headers: Mapping[str, str] | None,
154
+ ) -> dict[str, str]:
155
+ if headers is None:
156
+ return {}
157
+ if not isinstance(headers, Mapping):
158
+ raise TypeError("suggestions_headers must be a mapping of strings to strings.")
159
+
160
+ result: dict[str, str] = {}
161
+ for name, value in headers.items():
162
+ if not isinstance(name, str) or not isinstance(value, str):
163
+ raise TypeError("suggestions_headers must contain only string keys and values.")
164
+ result[name] = value
165
+ return result
166
+
167
+
114
168
  def sortable_multiselect(
115
169
  label: str,
116
170
  options: Sequence[str | Mapping[str, Any]],
@@ -128,6 +182,17 @@ def sortable_multiselect(
128
182
  selected_position: str = "bottom",
129
183
  icon_size: int = 20,
130
184
  options_max_height: int = 190,
185
+ suggestions_api_url: str | None = None,
186
+ suggestions_query_param: str = "q",
187
+ suggestions_response_path: str = "",
188
+ suggestions_label_path: str = "label",
189
+ suggestions_value_path: str = "value",
190
+ suggestions_icon_url_path: str | None = "icon_url",
191
+ suggestions_headers: Mapping[str, str] | None = None,
192
+ suggestions_min_chars: int = 1,
193
+ suggestions_debounce_ms: int = 300,
194
+ suggestions_loading_message: str = "Loading suggestions...",
195
+ suggestions_error_message: str = "Failed to load suggestions",
131
196
  key: str | None = None,
132
197
  ) -> list[str]:
133
198
  """Select multiple string values and return them in user-defined order.
@@ -166,6 +231,28 @@ def sortable_multiselect(
166
231
  Icon display size in pixels. Images keep their aspect ratio within this square size.
167
232
  options_max_height:
168
233
  Maximum height in pixels for the available options dropdown.
234
+ suggestions_api_url:
235
+ Absolute HTTP(S) endpoint used to fetch suggestions. None disables API suggestions.
236
+ suggestions_query_param:
237
+ Query parameter name used to send the current search text.
238
+ suggestions_response_path:
239
+ Dot-separated path to the options array in the JSON response. Empty means the root.
240
+ suggestions_label_path:
241
+ Dot-separated path to each suggestion's display label.
242
+ suggestions_value_path:
243
+ Dot-separated path to each suggestion's returned value.
244
+ suggestions_icon_url_path:
245
+ Optional dot-separated path to each suggestion's icon URL.
246
+ suggestions_headers:
247
+ HTTP headers sent by the browser. Do not include secret credentials.
248
+ suggestions_min_chars:
249
+ Minimum query length before requesting suggestions.
250
+ suggestions_debounce_ms:
251
+ Delay in milliseconds between input and the suggestions request.
252
+ suggestions_loading_message:
253
+ Text shown while suggestions are loading.
254
+ suggestions_error_message:
255
+ Text shown when suggestions cannot be loaded.
169
256
  key:
170
257
  Optional Streamlit component key.
171
258
  """
@@ -199,18 +286,58 @@ def sortable_multiselect(
199
286
  max_selection_count = _validate_max_selections(max_selections)
200
287
  icon_size_value = _validate_icon_size(icon_size)
201
288
  options_max_height_value = _validate_options_max_height(options_max_height)
202
-
203
- duplicate_options = sorted({value for value in option_values if option_values.count(value) > 1})
204
- if duplicate_options:
289
+ suggestions_api_url_value = _validate_suggestions_api_url(suggestions_api_url)
290
+ suggestions_query_param_value = _validate_non_empty_string(
291
+ "suggestions_query_param", suggestions_query_param
292
+ )
293
+ if not isinstance(suggestions_response_path, str):
294
+ raise TypeError("suggestions_response_path must be a string.")
295
+ suggestions_label_path_value = _validate_non_empty_string(
296
+ "suggestions_label_path", suggestions_label_path
297
+ )
298
+ suggestions_value_path_value = _validate_non_empty_string(
299
+ "suggestions_value_path", suggestions_value_path
300
+ )
301
+ suggestions_icon_url_path_value = _validate_optional_non_empty_string(
302
+ "suggestions_icon_url_path", suggestions_icon_url_path
303
+ )
304
+ suggestions_headers_value = _normalize_suggestions_headers(suggestions_headers)
305
+ suggestions_min_chars_value = _validate_non_negative_int(
306
+ "suggestions_min_chars", suggestions_min_chars
307
+ )
308
+ suggestions_debounce_ms_value = _validate_non_negative_int(
309
+ "suggestions_debounce_ms", suggestions_debounce_ms
310
+ )
311
+ if not isinstance(suggestions_loading_message, str):
312
+ raise TypeError("suggestions_loading_message must be a string.")
313
+ if not isinstance(suggestions_error_message, str):
314
+ raise TypeError("suggestions_error_message must be a string.")
315
+
316
+ # Set membership keeps these checks O(n); list.count / `in list` here are
317
+ # O(n**2) and hang the rerun once options reaches tens of thousands.
318
+ seen_option_values: set[str] = set()
319
+ has_duplicate_options = False
320
+ for value in option_values:
321
+ if value in seen_option_values:
322
+ has_duplicate_options = True
323
+ break
324
+ seen_option_values.add(value)
325
+ if has_duplicate_options:
205
326
  raise ValueError("options must not contain duplicate values.")
206
327
 
207
- missing_defaults = [value for value in default_values if value not in option_values]
328
+ missing_defaults = [value for value in default_values if value not in seen_option_values]
208
329
  if missing_defaults:
209
330
  missing = ", ".join(repr(value) for value in missing_defaults)
210
331
  raise ValueError(f"default contains values not present in options: {missing}.")
211
332
 
212
- duplicate_defaults = sorted({value for value in default_values if default_values.count(value) > 1})
213
- if duplicate_defaults:
333
+ seen_default_values: set[str] = set()
334
+ has_duplicate_defaults = False
335
+ for value in default_values:
336
+ if value in seen_default_values:
337
+ has_duplicate_defaults = True
338
+ break
339
+ seen_default_values.add(value)
340
+ if has_duplicate_defaults:
214
341
  raise ValueError("default must not contain duplicate values.")
215
342
 
216
343
  if max_selection_count is not None and len(default_values) > max_selection_count:
@@ -233,6 +360,17 @@ def sortable_multiselect(
233
360
  selected_position=selected_position,
234
361
  icon_size=icon_size_value,
235
362
  options_max_height=options_max_height_value,
363
+ suggestions_api_url=suggestions_api_url_value,
364
+ suggestions_query_param=suggestions_query_param_value,
365
+ suggestions_response_path=suggestions_response_path,
366
+ suggestions_label_path=suggestions_label_path_value,
367
+ suggestions_value_path=suggestions_value_path_value,
368
+ suggestions_icon_url_path=suggestions_icon_url_path_value,
369
+ suggestions_headers=suggestions_headers_value,
370
+ suggestions_min_chars=suggestions_min_chars_value,
371
+ suggestions_debounce_ms=suggestions_debounce_ms_value,
372
+ suggestions_loading_message=suggestions_loading_message,
373
+ suggestions_error_message=suggestions_error_message,
236
374
  key=key,
237
375
  default=default_values,
238
376
  )
@@ -0,0 +1 @@
1
+ :root{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#111827}body{margin:0}button,input{font:inherit}.sortable-multiselect{box-sizing:border-box;width:100%;padding:2px 0 6px}.component-label{display:block;margin:0 0 6px;color:#31333f;font-size:14px;font-weight:400}.search-combobox{position:relative}.search-input{box-sizing:border-box;width:100%;min-height:38px;border:1px solid #d1d5db;border-radius:6px;background:#fff;color:#111827;padding:6px 10px}.search-input:focus{border-color:#ff4b4b;box-shadow:0 0 0 1px #ff4b4b;outline:none}.search-input:disabled{background:#f9fafb;color:#6b7280}.options-popover{background:#fff;border:1px solid #d1d5db;border-radius:6px;box-shadow:0 6px 18px #31333f24;box-sizing:border-box;left:0;margin:4px 0 0;padding:4px;position:absolute;right:0;top:100%;z-index:10}.options-scroll{box-sizing:border-box;list-style:none;margin:0;max-height:var(--options-max-height, 190px);overflow-y:auto;padding:0}.options-inner{list-style:none;margin:0;padding:0}.option-item,.option-empty{align-items:center;border-radius:4px;box-sizing:border-box;color:#111827;display:flex;gap:8px;min-height:32px;padding:7px 9px}.option-item{cursor:pointer;overflow:hidden}.option-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.option-item.highlighted,.option-item:hover{background:#f3f4f6}.option-empty{color:#6b7280}.option-status{border-bottom:1px solid #e5e7eb;color:#6b7280;font-size:13px;padding:7px 9px}.option-error{color:#b91c1c}.selected-list{display:flex;flex-direction:column;gap:6px;list-style:none;margin:8px 0 0;padding:0}.sortable-item{align-items:center;background:var(--item-bg, #ffffff);border:1px solid #d1d5db;border-radius:6px;box-sizing:border-box;color:var(--item-fg, #111827);display:grid;gap:8px;grid-template-columns:34px minmax(0,1fr) auto;min-height:40px;padding:4px}.sortable-item.with-number{grid-template-columns:34px 30px minmax(0,1fr) auto}.sortable-item.dragging{border-color:#ff4b4b;box-shadow:0 4px 12px #31333f24}.drag-handle,.item-actions button{align-items:center;background:transparent;border:1px solid transparent;border-radius:4px;color:var(--item-muted-fg, #374151);cursor:pointer;display:inline-flex;height:30px;justify-content:center;min-width:30px;padding:0 7px}.drag-handle:hover,.item-actions button:hover{background:#f3f4f6}.drag-handle:focus,.item-actions button:focus{border-color:#ff4b4b;outline:none}.drag-handle:disabled,.item-actions button:disabled{color:color-mix(in srgb,var(--item-muted-fg, #374151) 45%,transparent);cursor:not-allowed}.item-content{align-items:center;display:inline-flex;gap:8px;min-width:0}.item-label{min-width:0;overflow-wrap:anywhere}.item-icon,.option-icon{border-radius:4px;flex:0 0 auto;height:var(--icon-size, 20px);object-fit:contain;width:var(--icon-size, 20px)}.item-number{align-items:center;background:color-mix(in srgb,var(--item-bg, #f3f4f6) 72%,#ffffff);border-radius:999px;color:var(--item-fg, #4b5563);display:inline-flex;font-size:12px;font-weight:600;height:24px;justify-content:center;line-height:1;min-width:24px;padding:0 6px}.item-actions{display:inline-flex;gap:2px}.empty-state{border:1px dashed #d1d5db;border-radius:6px;color:#6b7280;margin-top:8px;padding:10px;text-align:center}.selected-position-top .selected-list,.selected-position-top .empty-state{margin:0 0 8px}