@seekora-ai/ui-sdk-react 0.2.13 → 0.2.14

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 (74) hide show
  1. package/dist/components/CurrentRefinements.d.ts +22 -2
  2. package/dist/components/CurrentRefinements.d.ts.map +1 -1
  3. package/dist/components/CurrentRefinements.js +199 -47
  4. package/dist/components/Facets.d.ts +30 -1
  5. package/dist/components/Facets.d.ts.map +1 -1
  6. package/dist/components/Facets.js +418 -46
  7. package/dist/components/HierarchicalMenu.d.ts.map +1 -1
  8. package/dist/components/HierarchicalMenu.js +112 -4
  9. package/dist/components/Pagination.d.ts +47 -1
  10. package/dist/components/Pagination.d.ts.map +1 -1
  11. package/dist/components/Pagination.js +166 -28
  12. package/dist/components/RangeSlider.d.ts.map +1 -1
  13. package/dist/components/RangeSlider.js +49 -2
  14. package/dist/components/RichQuerySuggestions.d.ts +7 -0
  15. package/dist/components/RichQuerySuggestions.d.ts.map +1 -1
  16. package/dist/components/SearchBar.d.ts +16 -0
  17. package/dist/components/SearchBar.d.ts.map +1 -1
  18. package/dist/components/SearchBar.js +130 -16
  19. package/dist/components/SearchProvider.d.ts +8 -1
  20. package/dist/components/SearchProvider.d.ts.map +1 -1
  21. package/dist/components/SearchProvider.js +16 -4
  22. package/dist/components/SearchResults.d.ts +10 -0
  23. package/dist/components/SearchResults.d.ts.map +1 -1
  24. package/dist/components/SearchResults.js +9 -5
  25. package/dist/components/SortBy.d.ts +44 -4
  26. package/dist/components/SortBy.d.ts.map +1 -1
  27. package/dist/components/SortBy.js +154 -29
  28. package/dist/components/Stats.d.ts +14 -0
  29. package/dist/components/Stats.d.ts.map +1 -1
  30. package/dist/components/Stats.js +172 -23
  31. package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -1
  32. package/dist/components/suggestions/AmazonDropdown.js +2 -4
  33. package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -1
  34. package/dist/components/suggestions/GoogleDropdown.js +2 -6
  35. package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -1
  36. package/dist/components/suggestions/MinimalDropdown.js +2 -4
  37. package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -1
  38. package/dist/components/suggestions/MobileSheetDropdown.js +2 -4
  39. package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -1
  40. package/dist/components/suggestions/PinterestDropdown.js +2 -6
  41. package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -1
  42. package/dist/components/suggestions/ShopifyDropdown.js +2 -4
  43. package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -1
  44. package/dist/components/suggestions/SpotlightDropdown.js +2 -4
  45. package/dist/components/suggestions/utils.d.ts +10 -1
  46. package/dist/components/suggestions/utils.d.ts.map +1 -1
  47. package/dist/components/suggestions/utils.js +36 -0
  48. package/dist/components/suggestions-primitives/highlightMarkup.d.ts +16 -4
  49. package/dist/components/suggestions-primitives/highlightMarkup.d.ts.map +1 -1
  50. package/dist/components/suggestions-primitives/highlightMarkup.js +42 -4
  51. package/dist/hooks/useClickTracking.d.ts +36 -0
  52. package/dist/hooks/useClickTracking.d.ts.map +1 -0
  53. package/dist/hooks/useClickTracking.js +96 -0
  54. package/dist/hooks/useExperiment.d.ts +25 -0
  55. package/dist/hooks/useExperiment.d.ts.map +1 -0
  56. package/dist/hooks/useExperiment.js +146 -0
  57. package/dist/hooks/useKeyboardNavigation.d.ts +51 -0
  58. package/dist/hooks/useKeyboardNavigation.d.ts.map +1 -0
  59. package/dist/hooks/useKeyboardNavigation.js +113 -0
  60. package/dist/hooks/useQuerySuggestions.d.ts.map +1 -1
  61. package/dist/hooks/useQuerySuggestions.js +19 -3
  62. package/dist/hooks/useQuerySuggestionsEnhanced.d.ts.map +1 -1
  63. package/dist/hooks/useQuerySuggestionsEnhanced.js +23 -6
  64. package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
  65. package/dist/hooks/useSuggestionsAnalytics.js +6 -1
  66. package/dist/index.d.ts +4 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.umd.js +1 -1
  69. package/dist/src/index.d.ts +217 -16
  70. package/dist/src/index.esm.js +1586 -249
  71. package/dist/src/index.esm.js.map +1 -1
  72. package/dist/src/index.js +1585 -248
  73. package/dist/src/index.js.map +1 -1
  74. package/package.json +3 -3
@@ -9,7 +9,42 @@ import { useSearchState } from '../hooks/useSearchState';
9
9
  import { useQuerySuggestions } from '../hooks/useQuerySuggestions';
10
10
  import { log } from '@seekora-ai/ui-sdk-core';
11
11
  import { clsx } from 'clsx';
12
- export const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounceMs = 300, minQueryLength = 2, maxSuggestions = 10, onSearch, onQueryChange, onSuggestionSelect, onSearchStateChange, searchOptions, className, style, theme: customTheme, showLoadingState = false, renderSuggestion, renderLoading, }) => {
12
+ const SIZE_CONFIG = {
13
+ small: {
14
+ padding: '0.375rem 0.5rem',
15
+ fontSize: '0.875rem',
16
+ iconSize: 14,
17
+ iconPaddingLeft: '1.75rem',
18
+ iconPaddingRight: '1.75rem',
19
+ iconLeft: '0.5rem',
20
+ iconRight: '0.5rem',
21
+ },
22
+ medium: {
23
+ padding: '0.625rem 1rem',
24
+ fontSize: '1rem',
25
+ iconSize: 18,
26
+ iconPaddingLeft: '2.25rem',
27
+ iconPaddingRight: '2.25rem',
28
+ iconLeft: '0.625rem',
29
+ iconRight: '0.625rem',
30
+ },
31
+ large: {
32
+ padding: '0.875rem 1.25rem',
33
+ fontSize: '1.25rem',
34
+ iconSize: 22,
35
+ iconPaddingLeft: '2.75rem',
36
+ iconPaddingRight: '2.75rem',
37
+ iconLeft: '0.75rem',
38
+ iconRight: '0.75rem',
39
+ },
40
+ };
41
+ const DefaultSearchIcon = ({ size = 18 }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" },
42
+ React.createElement("circle", { cx: "11", cy: "11", r: "8" }),
43
+ React.createElement("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })));
44
+ const DefaultClearIcon = ({ size = 14 }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" },
45
+ React.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
46
+ React.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })));
47
+ export const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounceMs = 300, minQueryLength = 2, maxSuggestions = 10, onSearch, onQueryChange, onSuggestionSelect, onSearchStateChange, searchOptions, className, style, theme: customTheme, showLoadingState = false, renderSuggestion, renderLoading, renderSearchIcon, showClearButton = true, renderClearIcon, showSubmitButton = false, renderSubmitButton, size = 'medium', }) => {
13
48
  const { client, theme, enableAnalytics, autoTrackSearch } = useSearchContext();
14
49
  const { query, setQuery, search: triggerSearch, results, loading: searchLoading, error: searchError } = useSearchState();
15
50
  const [isFocused, setIsFocused] = useState(false);
@@ -150,28 +185,107 @@ export const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, d
150
185
  const processingTime = res?.processingTimeMS
151
186
  || res?.data?.processingTimeMS
152
187
  || res?.data?.data?.processingTimeMS;
188
+ // Size-based configuration
189
+ const sizeConfig = SIZE_CONFIG[size];
190
+ // Determine whether the search icon is shown (always unless renderSearchIcon returns null explicitly — but
191
+ // we always show it; there is no prop to hide the search icon, only to customise it)
192
+ const hasSearchIcon = true;
193
+ const hasClearBtn = showClearButton && query.length > 0;
194
+ // Compute input padding accounting for icons
195
+ const inputPaddingLeft = hasSearchIcon ? sizeConfig.iconPaddingLeft : sizeConfig.padding.split(' ')[1] || sizeConfig.padding;
196
+ const inputPaddingRight = hasClearBtn ? sizeConfig.iconPaddingRight : sizeConfig.padding.split(' ')[1] || sizeConfig.padding;
197
+ const borderRadius = typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium;
198
+ const focusBorderColor = isFocused ? theme.colors.focus : theme.colors.border;
199
+ const focusRingShadow = isFocused
200
+ ? `0 0 0 3px ${theme.colors.focus}33`
201
+ : undefined;
202
+ const handleClear = useCallback(() => {
203
+ setQuery('', false);
204
+ setSelectedIndex(-1);
205
+ inputRef.current?.focus();
206
+ }, [setQuery]);
207
+ const handleSubmit = useCallback(() => {
208
+ const currentValue = inputRef.current?.value || query;
209
+ handleSearch(currentValue);
210
+ }, [query, handleSearch]);
153
211
  return (React.createElement("div", { ref: containerRef, className: clsx(searchBarTheme.container, className), style: {
154
212
  position: 'relative',
155
213
  display: 'flex',
156
214
  alignItems: 'center',
215
+ // CSS custom properties for external styling
216
+ '--seekora-searchbar-bg': theme.colors.background,
217
+ '--seekora-searchbar-border': theme.colors.border,
218
+ '--seekora-searchbar-focus-border': theme.colors.focus,
219
+ '--seekora-searchbar-radius': borderRadius,
220
+ '--seekora-searchbar-icon-color': theme.colors.textSecondary,
157
221
  ...style,
158
222
  } },
159
- React.createElement("input", { ref: inputRef, type: "text", value: query, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder, className: clsx(searchBarTheme.input, isFocused && searchBarTheme.inputFocused), style: {
160
- flex: 1,
161
- padding: theme.spacing.medium,
162
- fontSize: theme.typography.fontSize.medium,
223
+ React.createElement("div", { style: { position: 'relative', flex: 1, display: 'flex', alignItems: 'center' } },
224
+ hasSearchIcon && (React.createElement("span", { className: searchBarTheme.searchIcon, "aria-hidden": "true", style: {
225
+ position: 'absolute',
226
+ left: sizeConfig.iconLeft,
227
+ top: '50%',
228
+ transform: 'translateY(-50%)',
229
+ display: 'flex',
230
+ alignItems: 'center',
231
+ justifyContent: 'center',
232
+ pointerEvents: 'none',
233
+ color: 'var(--seekora-searchbar-icon-color)',
234
+ zIndex: 1,
235
+ } }, renderSearchIcon ? renderSearchIcon() : React.createElement(DefaultSearchIcon, { size: sizeConfig.iconSize }))),
236
+ React.createElement("input", { ref: inputRef, type: "text", value: query, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder, className: clsx(searchBarTheme.input, isFocused && searchBarTheme.inputFocused), style: {
237
+ width: '100%',
238
+ paddingTop: sizeConfig.padding.split(' ')[0],
239
+ paddingBottom: sizeConfig.padding.split(' ')[0],
240
+ paddingLeft: inputPaddingLeft,
241
+ paddingRight: inputPaddingRight,
242
+ fontSize: sizeConfig.fontSize,
243
+ fontFamily: theme.typography.fontFamily,
244
+ backgroundColor: 'var(--seekora-searchbar-bg)',
245
+ color: theme.colors.text,
246
+ borderWidth: '1px',
247
+ borderStyle: 'solid',
248
+ borderColor: focusBorderColor,
249
+ borderRadius: 'var(--seekora-searchbar-radius)',
250
+ outline: 'none',
251
+ boxShadow: focusRingShadow,
252
+ transition: theme.transitions?.fast || '150ms ease-in-out',
253
+ boxSizing: 'border-box',
254
+ } }),
255
+ hasClearBtn && (React.createElement("button", { type: "button", onClick: handleClear, className: searchBarTheme.clearButton, "aria-label": "Clear search", style: {
256
+ position: 'absolute',
257
+ right: sizeConfig.iconRight,
258
+ top: '50%',
259
+ transform: 'translateY(-50%)',
260
+ display: 'flex',
261
+ alignItems: 'center',
262
+ justifyContent: 'center',
263
+ background: 'none',
264
+ border: 'none',
265
+ cursor: 'pointer',
266
+ padding: '2px',
267
+ borderRadius: '50%',
268
+ color: 'var(--seekora-searchbar-icon-color)',
269
+ transition: theme.transitions?.fast || '150ms ease-in-out',
270
+ zIndex: 1,
271
+ }, onMouseDown: (e) => {
272
+ // Prevent input blur so the clear action doesn't race with blur handler
273
+ e.preventDefault();
274
+ } }, renderClearIcon ? renderClearIcon() : React.createElement(DefaultClearIcon, { size: sizeConfig.iconSize - 4 })))),
275
+ showSubmitButton && (renderSubmitButton ? (React.createElement("div", { onClick: handleSubmit, style: { marginLeft: theme.spacing.small, cursor: 'pointer' } }, renderSubmitButton())) : (React.createElement("button", { type: "button", onClick: handleSubmit, className: searchBarTheme.submitButton, style: {
276
+ marginLeft: theme.spacing.small,
277
+ padding: sizeConfig.padding,
278
+ fontSize: sizeConfig.fontSize,
163
279
  fontFamily: theme.typography.fontFamily,
164
- backgroundColor: theme.colors.background,
165
- color: theme.colors.text,
166
- borderWidth: '1px',
167
- borderStyle: 'solid',
168
- borderColor: isFocused ? theme.colors.focus : theme.colors.border,
169
- borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
170
- outline: 'none',
171
- ...(isFocused && {
172
- boxShadow: theme.shadows.small,
173
- }),
174
- } }),
280
+ fontWeight: theme.typography.fontWeight?.medium ?? 500,
281
+ backgroundColor: theme.colors.primary,
282
+ color: '#ffffff',
283
+ border: 'none',
284
+ borderRadius: 'var(--seekora-searchbar-radius)',
285
+ cursor: 'pointer',
286
+ whiteSpace: 'nowrap',
287
+ transition: theme.transitions?.fast || '150ms ease-in-out',
288
+ } }, "Search"))),
175
289
  processingTime !== undefined && (React.createElement("span", { style: {
176
290
  marginLeft: theme.spacing.small,
177
291
  fontSize: theme.typography.fontSize.small,
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * SearchProvider Component
3
3
  *
4
- * Provides Seekora client and context to child components
4
+ * Provides Seekora client and context to child components.
5
+ * Supports A/B testing via abTestId/abVariant props.
5
6
  */
6
7
  import React, { ReactNode } from 'react';
7
8
  import type { SeekoraClient } from '@seekora-ai/search-sdk';
@@ -21,6 +22,12 @@ export interface SearchProviderProps {
21
22
  autoTrackSearch?: boolean;
22
23
  stateManager?: SearchStateManagerConfig;
23
24
  children: ReactNode;
25
+ /** A/B test experiment ID to include in all analytics events */
26
+ abTestId?: string;
27
+ /** A/B test variant to include in all analytics events */
28
+ abVariant?: string;
29
+ /** Auto-fetch experiment assignments on mount (default: false) */
30
+ experiments?: boolean;
24
31
  }
25
32
  export declare const SearchProvider: React.FC<SearchProviderProps>;
26
33
  export declare const useSearchContext: () => SearchContextValue;
@@ -1 +1 @@
1
- {"version":3,"file":"SearchProvider.d.ts","sourceRoot":"","sources":["../../src/components/SearchProvider.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,EAA6B,SAAS,EAAW,MAAM,OAAO,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAO,kBAAkB,EAAE,KAAK,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACjG,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI1D,UAAU,kBAAkB;IAC1B,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC;IACb,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,EAAE,kBAAkB,CAAC;CAClC;AAID,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,wBAAwB,CAAC;IACxC,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAqCxD,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,kBAQnC,CAAC"}
1
+ {"version":3,"file":"SearchProvider.d.ts","sourceRoot":"","sources":["../../src/components/SearchProvider.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAA6B,SAAS,EAAsB,MAAM,OAAO,CAAC;AACxF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAO,kBAAkB,EAAE,KAAK,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACjG,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI1D,UAAU,kBAAkB;IAC1B,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC;IACb,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,EAAE,kBAAkB,CAAC;CAClC;AAID,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,wBAAwB,CAAC;IACxC,QAAQ,EAAE,SAAS,CAAC;IACpB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAoDxD,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,kBAQnC,CAAC"}
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * SearchProvider Component
3
3
  *
4
- * Provides Seekora client and context to child components
4
+ * Provides Seekora client and context to child components.
5
+ * Supports A/B testing via abTestId/abVariant props.
5
6
  */
6
- import React, { createContext, useContext, useMemo } from 'react';
7
+ import React, { createContext, useContext, useMemo, useEffect } from 'react';
7
8
  import { log, SearchStateManager } from '@seekora-ai/ui-sdk-core';
8
9
  import { defaultTheme } from '../themes/default';
9
10
  import { createTheme } from '../themes/createTheme';
10
11
  const SearchContext = createContext(null);
11
- export const SearchProvider = ({ client, theme: themeConfig, enableAnalytics = true, autoTrackSearch = true, stateManager: stateManagerConfig, children, }) => {
12
+ export const SearchProvider = ({ client, theme: themeConfig, enableAnalytics = true, autoTrackSearch = true, stateManager: stateManagerConfig, children, abTestId, abVariant, experiments: _experiments, }) => {
12
13
  const theme = useMemo(() => {
13
14
  return themeConfig ? createTheme(themeConfig) : defaultTheme;
14
15
  }, [themeConfig]);
@@ -21,8 +22,19 @@ export const SearchProvider = ({ client, theme: themeConfig, enableAnalytics = t
21
22
  itemsPerPage: 10,
22
23
  defaultSearchOptions: { widget_mode: true },
23
24
  ...stateManagerConfig,
25
+ abTestId,
26
+ abVariant,
24
27
  });
25
- }, [client, stateManagerConfig]);
28
+ }, [client, stateManagerConfig, abTestId, abVariant]);
29
+ // Update A/B test fields on state manager and SDK client when props change
30
+ useEffect(() => {
31
+ if (abTestId !== undefined || abVariant !== undefined) {
32
+ stateManager.setAbTest(abTestId, abVariant);
33
+ if (typeof client.setAbTest === 'function') {
34
+ client.setAbTest(abTestId, abVariant);
35
+ }
36
+ }
37
+ }, [stateManager, client, abTestId, abVariant]);
26
38
  const value = useMemo(() => ({
27
39
  client,
28
40
  theme,
@@ -21,6 +21,10 @@ export interface SearchResultsTheme {
21
21
  loadingState?: string;
22
22
  errorState?: string;
23
23
  pagination?: string;
24
+ /** Custom min-height class override */
25
+ minHeight?: string;
26
+ /** Custom min-width class override */
27
+ minWidth?: string;
24
28
  }
25
29
  export interface SearchResultsProps {
26
30
  results?: SearchResponse | null;
@@ -48,6 +52,12 @@ export interface SearchResultsProps {
48
52
  enableKeyboardNavigation?: boolean;
49
53
  /** Auto-focus the results container */
50
54
  autoFocus?: boolean;
55
+ /** Minimum height to prevent container collapse when empty (default: '400px') */
56
+ minHeight?: string;
57
+ /** Minimum width to prevent container shrinking (default: '100%') */
58
+ minWidth?: string;
59
+ /** Opacity applied to results while loading new results (default: 0.7, set to 1 to disable) */
60
+ loadingOpacity?: number;
51
61
  }
52
62
  export declare const SearchResults: React.FC<SearchResultsProps>;
53
63
  //# sourceMappingURL=SearchResults.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SearchResults.d.ts","sourceRoot":"","sources":["../../src/components/SearchResults.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEnF,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC;IAC1F,WAAW,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IACpC,uHAAuH;IACvH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IACtC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,KAAK,CAAC,SAAS,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,mEAAmE;IACnE,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,gFAAgF;IAChF,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,GAAG,EAAE,CAAC;IAC1C,+DAA+D;IAC/D,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAoBD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAsmBtD,CAAC"}
1
+ {"version":3,"file":"SearchResults.d.ts","sourceRoot":"","sources":["../../src/components/SearchResults.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEnF,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC;IAC1F,WAAW,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IACpC,uHAAuH;IACvH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IACtC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,KAAK,CAAC,SAAS,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,mEAAmE;IACnE,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,gFAAgF;IAChF,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,GAAG,EAAE,CAAC;IAC1C,+DAA+D;IAC/D,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+FAA+F;IAC/F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAoBD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA6mBtD,CAAC"}
@@ -29,7 +29,7 @@ const formatPrice = (value, currency = '₹') => {
29
29
  }
30
30
  return String(value);
31
31
  };
32
- export const SearchResults = ({ results: resultsProp, loading: loadingProp, error: errorProp, onResultClick, renderResult, renderEmpty, showLoadingState = false, renderLoading, renderError, className, style, theme: customTheme, itemsPerPage = 20, showPagination = false, viewMode = 'list', fieldMapping, extractResults, enableKeyboardNavigation = true, autoFocus = false, }) => {
32
+ export const SearchResults = ({ results: resultsProp, loading: loadingProp, error: errorProp, onResultClick, renderResult, renderEmpty, showLoadingState = false, renderLoading, renderError, className, style, theme: customTheme, itemsPerPage = 20, showPagination = false, viewMode = 'list', fieldMapping, extractResults, enableKeyboardNavigation = true, autoFocus = false, minHeight = '400px', minWidth = '100%', loadingOpacity = 0.7, }) => {
33
33
  const { theme, client, enableAnalytics } = useSearchContext();
34
34
  const { results: stateResults, loading: stateLoading, error: stateError, currentPage, itemsPerPage: stateItemsPerPage } = useSearchState();
35
35
  const searchResultsTheme = customTheme || {};
@@ -373,6 +373,10 @@ export const SearchResults = ({ results: resultsProp, loading: loadingProp, erro
373
373
  });
374
374
  // Determine container style based on view mode
375
375
  const containerStyle = {
376
+ minHeight: `var(--seekora-results-min-height, ${minHeight})`,
377
+ minWidth: `var(--seekora-results-min-width, ${minWidth})`,
378
+ transition: 'opacity 200ms ease-in-out',
379
+ opacity: loading && resultItems.length > 0 ? loadingOpacity : 1,
376
380
  ...style,
377
381
  };
378
382
  // Determine results list style based on view mode
@@ -399,19 +403,19 @@ export const SearchResults = ({ results: resultsProp, loading: loadingProp, erro
399
403
  // When loading with no previous results, show loading only if showLoadingState (default: show previous results, no loading screen)
400
404
  if (loading && resultItems.length === 0 && showLoadingState) {
401
405
  log.verbose('SearchResults: Rendering loading state');
402
- return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: style }, renderLoading ? renderLoading() : defaultRenderLoading()));
406
+ return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: containerStyle }, renderLoading ? renderLoading() : defaultRenderLoading()));
403
407
  }
404
- // When loading with previous results, fall through and render them (no loading screen)
408
+ // When loading with previous results, fall through and render them (with opacity transition)
405
409
  if (error) {
406
410
  log.error('SearchResults: Rendering error state', {
407
411
  error: error.message,
408
412
  stack: error.stack,
409
413
  });
410
- return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: style }, renderError ? renderError(error) : defaultRenderError(error)));
414
+ return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: containerStyle }, renderError ? renderError(error) : defaultRenderError(error)));
411
415
  }
412
416
  if (!results || resultItems.length === 0) {
413
417
  log.verbose('SearchResults: No results to display');
414
- return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: style }, renderEmpty ? renderEmpty() : defaultRenderEmpty()));
418
+ return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: containerStyle }, renderEmpty ? renderEmpty() : defaultRenderEmpty()));
415
419
  }
416
420
  const renderFn = renderResult || defaultRenderResult;
417
421
  return (React.createElement("div", { ref: containerRef, className: clsx(searchResultsTheme.container, className), style: containerStyle, tabIndex: enableKeyboardNavigation ? 0 : undefined, onKeyDown: handleKeyDown, role: "listbox", "aria-label": "Search results", "aria-activedescendant": activeIndex >= 0 ? `result-${activeIndex}` : undefined },
@@ -1,18 +1,50 @@
1
1
  /**
2
2
  * SortBy Component
3
3
  *
4
- * Displays sort options for search results
5
- * Integrates with SearchStateManager for automatic state sync
4
+ * Displays sort options for search results in multiple display variants:
5
+ * - dropdown (default) native select element
6
+ * - button-group — horizontal row of toggle buttons
7
+ * - radio-group — vertical list of radio inputs
8
+ *
9
+ * Integrates with SearchStateManager for automatic state sync.
10
+ *
11
+ * CSS Variables (apply on a parent element to customize):
12
+ * --seekora-sort-bg — background color
13
+ * --seekora-sort-color — text color
14
+ * --seekora-sort-border — border color
15
+ * --seekora-sort-active-bg — active item background
16
+ * --seekora-sort-active-color — active item text color
6
17
  */
7
18
  import React from 'react';
8
19
  export interface SortOption {
9
20
  value: string;
10
21
  label: string;
11
22
  }
23
+ export type SortByVariant = 'dropdown' | 'button-group' | 'radio-group';
24
+ export type SortBySize = 'small' | 'medium' | 'large';
12
25
  export interface SortByTheme {
26
+ /** Root container */
13
27
  container?: string;
28
+ /** Dropdown select element */
14
29
  select?: string;
30
+ /** Dropdown option element */
15
31
  option?: string;
32
+ /** Optional label above the control */
33
+ label?: string;
34
+ /** Button group wrapper */
35
+ buttonGroup?: string;
36
+ /** Individual button in the group */
37
+ buttonGroupItem?: string;
38
+ /** Active button in the group */
39
+ buttonGroupItemActive?: string;
40
+ /** Radio group wrapper */
41
+ radioGroup?: string;
42
+ /** Individual radio row (input + label) */
43
+ radioItem?: string;
44
+ /** Active radio row */
45
+ radioItemActive?: string;
46
+ /** Radio label text */
47
+ radioLabel?: string;
16
48
  }
17
49
  export interface SortByProps {
18
50
  /** Available sort options */
@@ -23,7 +55,7 @@ export interface SortByProps {
23
55
  defaultValue?: string;
24
56
  /** Callback when sort changes */
25
57
  onSortChange?: (value: string) => void;
26
- /** Custom render function for select */
58
+ /** Custom render function for select (only used in dropdown variant) */
27
59
  renderSelect?: (props: {
28
60
  value: string;
29
61
  onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
@@ -35,10 +67,18 @@ export interface SortByProps {
35
67
  style?: React.CSSProperties;
36
68
  /** Custom theme */
37
69
  theme?: SortByTheme;
38
- /** Placeholder text */
70
+ /** Placeholder text (dropdown variant) */
39
71
  placeholder?: string;
40
72
  /** Whether to sync with SearchStateManager (default: true) */
41
73
  syncWithState?: boolean;
74
+ /** Display variant (default: 'dropdown') */
75
+ variant?: SortByVariant;
76
+ /** Optional label text displayed above the control */
77
+ label?: string;
78
+ /** Whether to show the label (default: true when label is provided) */
79
+ showLabel?: boolean;
80
+ /** Size variant affecting padding and font size (default: 'medium') */
81
+ size?: SortBySize;
42
82
  }
43
83
  export declare const SortBy: React.FC<SortByProps>;
44
84
  //# sourceMappingURL=SortBy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SortBy.d.ts","sourceRoot":"","sources":["../../src/components/SortBy.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAoB,MAAM,OAAO,CAAC;AAKzC,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,6BAA6B;IAC7B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,wCAAwC;IACxC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;QACrB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;QAC5D,OAAO,EAAE,UAAU,EAAE,CAAC;KACvB,KAAK,KAAK,CAAC,SAAS,CAAC;IACtB,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,mBAAmB;IACnB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CA8FxC,CAAC"}
1
+ {"version":3,"file":"SortBy.d.ts","sourceRoot":"","sources":["../../src/components/SortBy.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAwC,MAAM,OAAO,CAAC;AAS7D,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,cAAc,GAAG,aAAa,CAAC;AACxE,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,qBAAqB;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2BAA2B;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,0BAA0B;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uBAAuB;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uBAAuB;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,6BAA6B;IAC7B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,wEAAwE;IACxE,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;QACrB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;QAC5D,OAAO,EAAE,UAAU,EAAE,CAAC;KACvB,KAAK,KAAK,CAAC,SAAS,CAAC;IACtB,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,mBAAmB;IACnB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uEAAuE;IACvE,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AA8BD,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAmSxC,CAAC"}
@@ -1,61 +1,186 @@
1
1
  /**
2
2
  * SortBy Component
3
3
  *
4
- * Displays sort options for search results
5
- * Integrates with SearchStateManager for automatic state sync
4
+ * Displays sort options for search results in multiple display variants:
5
+ * - dropdown (default) native select element
6
+ * - button-group — horizontal row of toggle buttons
7
+ * - radio-group — vertical list of radio inputs
8
+ *
9
+ * Integrates with SearchStateManager for automatic state sync.
10
+ *
11
+ * CSS Variables (apply on a parent element to customize):
12
+ * --seekora-sort-bg — background color
13
+ * --seekora-sort-color — text color
14
+ * --seekora-sort-border — border color
15
+ * --seekora-sort-active-bg — active item background
16
+ * --seekora-sort-active-color — active item text color
6
17
  */
7
- import React, { useEffect } from 'react';
18
+ import React, { useCallback, useEffect, useId } from 'react';
8
19
  import { useSearchContext } from './SearchProvider';
9
20
  import { useSearchState } from '../hooks/useSearchState';
10
21
  import { clsx } from 'clsx';
11
- export const SortBy = ({ options, value: valueProp, defaultValue, onSortChange, renderSelect, className, style, theme: customTheme, placeholder = 'Sort by...', syncWithState = true, }) => {
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+ /** Map a size token to theme fontSize and spacing values. */
26
+ function sizeStyles(size, theme) {
27
+ switch (size) {
28
+ case 'small':
29
+ return { fontSize: theme.typography.fontSize.small, padding: theme.spacing.small };
30
+ case 'large':
31
+ return { fontSize: theme.typography.fontSize.large, padding: theme.spacing.medium };
32
+ case 'medium':
33
+ default:
34
+ return { fontSize: theme.typography.fontSize.medium, padding: theme.spacing.small };
35
+ }
36
+ }
37
+ function resolveBorderRadius(br) {
38
+ return typeof br === 'string' ? br : br.medium;
39
+ }
40
+ // ---------------------------------------------------------------------------
41
+ // Component
42
+ // ---------------------------------------------------------------------------
43
+ export const SortBy = ({ options, value: valueProp, defaultValue, onSortChange, renderSelect, className, style, theme: customTheme, placeholder = 'Sort by...', syncWithState = true, variant = 'dropdown', label, showLabel, size = 'medium', }) => {
12
44
  const { theme } = useSearchContext();
13
45
  const { sortBy: stateManagerSortBy, setSortBy } = useSearchState();
14
46
  const sortByTheme = customTheme || {};
15
- // Initialize with defaultValue or first option
47
+ const instanceId = useId();
48
+ // Determine whether the label should render.
49
+ const shouldShowLabel = showLabel !== undefined ? showLabel : !!label;
50
+ // ------ State ----------------------------------------------------------
16
51
  const [internalValue, setInternalValue] = React.useState(defaultValue || options[0]?.value || '');
17
52
  // Sync with StateManager on mount if defaultValue is set
18
53
  useEffect(() => {
19
54
  if (syncWithState && defaultValue && !stateManagerSortBy) {
20
55
  setSortBy(defaultValue, false); // Don't trigger search on initial sync
21
56
  }
22
- }, []);
57
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
23
58
  // Determine the current value: controlled prop > StateManager > internal
24
59
  const value = valueProp !== undefined
25
60
  ? valueProp
26
- : (syncWithState && stateManagerSortBy)
61
+ : syncWithState && stateManagerSortBy
27
62
  ? stateManagerSortBy
28
63
  : internalValue;
64
+ // ------ Handlers -------------------------------------------------------
29
65
  const handleChange = (e) => {
30
- const newValue = e.target.value;
66
+ applyValue(e.target.value);
67
+ };
68
+ const applyValue = useCallback((newValue) => {
31
69
  setInternalValue(newValue);
32
- // Update StateManager (automatically triggers search)
33
70
  if (syncWithState) {
34
71
  setSortBy(newValue);
35
72
  }
36
- // Call callback for backwards compatibility
37
73
  if (onSortChange) {
38
74
  onSortChange(newValue);
39
75
  }
76
+ }, [syncWithState, setSortBy, onSortChange]);
77
+ // ------ Derived styles -------------------------------------------------
78
+ const { fontSize, padding } = sizeStyles(size, theme);
79
+ const borderRadius = resolveBorderRadius(theme.borderRadius);
80
+ const cssVarStyle = {
81
+ '--seekora-sort-bg': theme.colors.background,
82
+ '--seekora-sort-color': theme.colors.text,
83
+ '--seekora-sort-border': theme.colors.border,
84
+ '--seekora-sort-active-bg': theme.colors.primary,
85
+ '--seekora-sort-active-color': theme.colors.background,
40
86
  };
41
- const defaultRenderSelect = () => (React.createElement("select", { value: value, onChange: handleChange, className: clsx(sortByTheme.select, className), style: {
42
- padding: theme.spacing.small,
43
- paddingRight: theme.spacing.medium,
44
- fontSize: theme.typography.fontSize.medium,
45
- border: `1px solid ${theme.colors.border}`,
46
- borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
47
- backgroundColor: theme.colors.background,
48
- color: theme.colors.text,
49
- cursor: 'pointer',
50
- outline: 'none',
51
- ...style,
52
- }, "aria-label": "Sort results" }, options.map((option) => (React.createElement("option", { key: option.value, value: option.value, className: sortByTheme.option }, option.label)))));
53
- if (renderSelect) {
54
- return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: style }, renderSelect({
55
- value,
56
- onChange: handleChange,
57
- options,
58
- })));
87
+ // ------ Label ----------------------------------------------------------
88
+ const labelElement = shouldShowLabel && label ? (React.createElement("span", { className: clsx(sortByTheme.label), style: {
89
+ display: 'block',
90
+ marginBottom: theme.spacing.small,
91
+ fontSize,
92
+ color: 'var(--seekora-sort-color)',
93
+ fontWeight: 500,
94
+ } }, label)) : null;
95
+ // ------ Dropdown variant (original) ------------------------------------
96
+ if (variant === 'dropdown') {
97
+ if (renderSelect) {
98
+ return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
99
+ labelElement,
100
+ renderSelect({
101
+ value,
102
+ onChange: handleChange,
103
+ options,
104
+ })));
105
+ }
106
+ return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
107
+ labelElement,
108
+ React.createElement("select", { value: value, onChange: handleChange, className: clsx(sortByTheme.select), style: {
109
+ padding,
110
+ paddingRight: theme.spacing.medium,
111
+ fontSize,
112
+ border: '1px solid var(--seekora-sort-border)',
113
+ borderRadius,
114
+ backgroundColor: 'var(--seekora-sort-bg)',
115
+ color: 'var(--seekora-sort-color)',
116
+ cursor: 'pointer',
117
+ outline: 'none',
118
+ width: '100%',
119
+ }, "aria-label": label || 'Sort results' }, options.map((option) => (React.createElement("option", { key: option.value, value: option.value, className: sortByTheme.option }, option.label))))));
120
+ }
121
+ // ------ Button group variant -------------------------------------------
122
+ if (variant === 'button-group') {
123
+ return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
124
+ labelElement,
125
+ React.createElement("div", { role: "group", "aria-label": label || 'Sort results', className: clsx(sortByTheme.buttonGroup), style: {
126
+ display: 'inline-flex',
127
+ borderRadius,
128
+ overflow: 'hidden',
129
+ border: '1px solid var(--seekora-sort-border)',
130
+ } }, options.map((option, index) => {
131
+ const isActive = option.value === value;
132
+ return (React.createElement("button", { key: option.value, type: "button", role: "button", "aria-pressed": isActive, onClick: () => applyValue(option.value), className: clsx(sortByTheme.buttonGroupItem, isActive && sortByTheme.buttonGroupItemActive), style: {
133
+ padding,
134
+ fontSize,
135
+ border: 'none',
136
+ borderRight: index < options.length - 1
137
+ ? '1px solid var(--seekora-sort-border)'
138
+ : 'none',
139
+ backgroundColor: isActive
140
+ ? 'var(--seekora-sort-active-bg)'
141
+ : 'var(--seekora-sort-bg)',
142
+ color: isActive
143
+ ? 'var(--seekora-sort-active-color)'
144
+ : 'var(--seekora-sort-color)',
145
+ cursor: 'pointer',
146
+ fontWeight: isActive ? 600 : 400,
147
+ transition: 'background-color 0.15s ease, color 0.15s ease',
148
+ outline: 'none',
149
+ } }, option.label));
150
+ }))));
151
+ }
152
+ // ------ Radio group variant --------------------------------------------
153
+ if (variant === 'radio-group') {
154
+ const radioName = `seekora-sort-${instanceId}`;
155
+ return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
156
+ labelElement,
157
+ React.createElement("div", { role: "radiogroup", "aria-label": label || 'Sort results', className: clsx(sortByTheme.radioGroup), style: {
158
+ display: 'flex',
159
+ flexDirection: 'column',
160
+ gap: theme.spacing.small,
161
+ } }, options.map((option) => {
162
+ const isActive = option.value === value;
163
+ const radioId = `${radioName}-${option.value}`;
164
+ return (React.createElement("label", { key: option.value, htmlFor: radioId, className: clsx(sortByTheme.radioItem, isActive && sortByTheme.radioItemActive), style: {
165
+ display: 'flex',
166
+ alignItems: 'center',
167
+ gap: theme.spacing.small,
168
+ padding,
169
+ borderRadius,
170
+ cursor: 'pointer',
171
+ backgroundColor: isActive
172
+ ? 'var(--seekora-sort-active-bg)'
173
+ : 'transparent',
174
+ color: isActive
175
+ ? 'var(--seekora-sort-active-color)'
176
+ : 'var(--seekora-sort-color)',
177
+ fontWeight: isActive ? 600 : 400,
178
+ transition: 'background-color 0.15s ease, color 0.15s ease',
179
+ } },
180
+ React.createElement("input", { type: "radio", id: radioId, name: radioName, value: option.value, checked: isActive, onChange: () => applyValue(option.value), style: { margin: 0 } }),
181
+ React.createElement("span", { className: clsx(sortByTheme.radioLabel), style: { fontSize } }, option.label)));
182
+ }))));
59
183
  }
60
- return defaultRenderSelect();
184
+ // Fallback — should never reach here, but satisfies TS exhaustiveness
185
+ return null;
61
186
  };