@jmruthers/pace-core 0.5.75 → 0.5.76

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 (226) hide show
  1. package/dist/{DataTable-HWZQGASI.js → DataTable-4GAVPIEG.js} +48 -30
  2. package/dist/{PublicLoadingSpinner-BKNBT6b6.d.ts → PublicLoadingSpinner-BiNER8F5.d.ts} +28 -17
  3. package/dist/{chunk-33PHABLB.js → chunk-AFGTSUAD.js} +10 -127
  4. package/dist/chunk-AFGTSUAD.js.map +1 -0
  5. package/dist/{chunk-2DFZ432F.js → chunk-K34IM5CT.js} +3 -5
  6. package/dist/{chunk-2DFZ432F.js.map → chunk-K34IM5CT.js.map} +1 -1
  7. package/dist/{chunk-2CHATWBF.js → chunk-KHJS6VIA.js} +199 -35
  8. package/dist/chunk-KHJS6VIA.js.map +1 -0
  9. package/dist/{chunk-ZTT2AXMX.js → chunk-KK73ZB4E.js} +2 -2
  10. package/dist/{chunk-CY3AHGO4.js → chunk-M5IWZRBT.js} +1750 -2815
  11. package/dist/chunk-M5IWZRBT.js.map +1 -0
  12. package/dist/{chunk-DAXLNIDY.js → chunk-Y6TXWPJO.js} +6 -4
  13. package/dist/{chunk-DAXLNIDY.js.map → chunk-Y6TXWPJO.js.map} +1 -1
  14. package/dist/{chunk-YNUBMSMV.js → chunk-YCKPEMJA.js} +186 -263
  15. package/dist/chunk-YCKPEMJA.js.map +1 -0
  16. package/dist/components.d.ts +1 -1
  17. package/dist/components.js +7 -6
  18. package/dist/components.js.map +1 -1
  19. package/dist/hooks.d.ts +17 -40
  20. package/dist/hooks.js +6 -6
  21. package/dist/index.d.ts +3 -3
  22. package/dist/index.js +12 -10
  23. package/dist/index.js.map +1 -1
  24. package/dist/rbac/index.d.ts +54 -1
  25. package/dist/rbac/index.js +5 -4
  26. package/dist/utils.js +1 -1
  27. package/docs/TERMINOLOGY.md +231 -0
  28. package/docs/api/classes/ColumnFactory.md +1 -1
  29. package/docs/api/classes/ErrorBoundary.md +1 -1
  30. package/docs/api/classes/InvalidScopeError.md +1 -1
  31. package/docs/api/classes/MissingUserContextError.md +1 -1
  32. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  33. package/docs/api/classes/PermissionDeniedError.md +1 -1
  34. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  35. package/docs/api/classes/RBACAuditManager.md +1 -1
  36. package/docs/api/classes/RBACCache.md +1 -1
  37. package/docs/api/classes/RBACEngine.md +1 -1
  38. package/docs/api/classes/RBACError.md +1 -1
  39. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  40. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  41. package/docs/api/classes/StorageUtils.md +1 -1
  42. package/docs/api/enums/FileCategory.md +1 -1
  43. package/docs/api/interfaces/AggregateConfig.md +1 -1
  44. package/docs/api/interfaces/ButtonProps.md +1 -1
  45. package/docs/api/interfaces/CardProps.md +1 -1
  46. package/docs/api/interfaces/ColorPalette.md +1 -1
  47. package/docs/api/interfaces/ColorShade.md +1 -1
  48. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  49. package/docs/api/interfaces/DataTableAction.md +1 -1
  50. package/docs/api/interfaces/DataTableColumn.md +1 -1
  51. package/docs/api/interfaces/DataTableProps.md +1 -1
  52. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  53. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  54. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  55. package/docs/api/interfaces/EventLogoProps.md +1 -1
  56. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  57. package/docs/api/interfaces/FileMetadata.md +1 -1
  58. package/docs/api/interfaces/FileReference.md +1 -1
  59. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  60. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  61. package/docs/api/interfaces/FileUploadProps.md +1 -1
  62. package/docs/api/interfaces/FooterProps.md +1 -1
  63. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  64. package/docs/api/interfaces/InputProps.md +1 -1
  65. package/docs/api/interfaces/LabelProps.md +1 -1
  66. package/docs/api/interfaces/LoginFormProps.md +1 -1
  67. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  68. package/docs/api/interfaces/NavigationContextType.md +1 -1
  69. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  70. package/docs/api/interfaces/NavigationItem.md +1 -1
  71. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  72. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  73. package/docs/api/interfaces/Organisation.md +1 -1
  74. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  75. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  76. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  77. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  78. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  79. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  80. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  81. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  82. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  83. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  84. package/docs/api/interfaces/PaletteData.md +1 -1
  85. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  86. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  87. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  88. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  89. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  90. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  91. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  92. package/docs/api/interfaces/RBACConfig.md +1 -1
  93. package/docs/api/interfaces/RBACContextType.md +1 -1
  94. package/docs/api/interfaces/RBACLogger.md +1 -1
  95. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  96. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  97. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  98. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  99. package/docs/api/interfaces/RouteConfig.md +1 -1
  100. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  101. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  102. package/docs/api/interfaces/StorageConfig.md +1 -1
  103. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  104. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  105. package/docs/api/interfaces/StorageListOptions.md +1 -1
  106. package/docs/api/interfaces/StorageListResult.md +1 -1
  107. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  108. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  109. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  110. package/docs/api/interfaces/StyleImport.md +1 -1
  111. package/docs/api/interfaces/SwitchProps.md +1 -1
  112. package/docs/api/interfaces/ToastActionElement.md +1 -1
  113. package/docs/api/interfaces/ToastProps.md +1 -1
  114. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  115. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  116. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  117. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  118. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  119. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  120. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  121. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  122. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  123. package/docs/api/interfaces/UseResolvedScopeOptions.md +47 -0
  124. package/docs/api/interfaces/UseResolvedScopeReturn.md +47 -0
  125. package/docs/api/interfaces/UserEventAccess.md +1 -1
  126. package/docs/api/interfaces/UserMenuProps.md +1 -1
  127. package/docs/api/interfaces/UserProfile.md +1 -1
  128. package/docs/api/modules.md +57 -11
  129. package/docs/api-reference/providers.md +26 -7
  130. package/docs/best-practices/README.md +20 -0
  131. package/docs/best-practices/accessibility.md +566 -0
  132. package/docs/best-practices/performance-expansion.md +473 -0
  133. package/docs/core-concepts/authentication.md +15 -7
  134. package/docs/documentation-index.md +1 -1
  135. package/docs/documentation-templates.md +539 -0
  136. package/docs/getting-started/quick-start.md +16 -66
  137. package/docs/implementation-guides/component-styling.md +410 -0
  138. package/docs/implementation-guides/data-tables.md +1 -1
  139. package/docs/style-guide.md +39 -0
  140. package/package.json +1 -1
  141. package/src/__tests__/TEST_GUIDE_CURSOR.md +290 -0
  142. package/src/__tests__/helpers/supabaseMock.ts +48 -2
  143. package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +17 -6
  144. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +73 -9
  145. package/src/components/DataTable/components/DataTableCore.tsx +280 -475
  146. package/src/components/DataTable/components/UnifiedTableBody.tsx +120 -153
  147. package/src/components/DataTable/components/index.ts +1 -2
  148. package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +208 -275
  149. package/src/components/DataTable/core/index.ts +1 -8
  150. package/src/components/DataTable/hooks/__tests__/useColumnOrderPersistence.test.ts +525 -0
  151. package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +570 -0
  152. package/src/components/DataTable/hooks/__tests__/useHierarchicalState.test.ts +214 -0
  153. package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +224 -0
  154. package/src/components/DataTable/hooks/index.ts +6 -0
  155. package/src/components/DataTable/hooks/useColumnReordering.ts +1 -0
  156. package/src/components/DataTable/hooks/useDataTablePermissions.ts +149 -0
  157. package/src/components/DataTable/hooks/useDataTableState.ts +12 -6
  158. package/src/components/DataTable/hooks/useHierarchicalState.ts +26 -8
  159. package/src/components/DataTable/hooks/useTableColumns.ts +153 -0
  160. package/src/components/DataTable/index.ts +1 -9
  161. package/src/components/DataTable/utils/__tests__/COVERAGE_NOTE.md +89 -0
  162. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +3 -6
  163. package/src/components/DataTable/utils/__tests__/flexibleImport.test.ts +462 -0
  164. package/src/components/DataTable/utils/__tests__/hierarchicalSorting.test.ts +247 -0
  165. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +8 -6
  166. package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +466 -0
  167. package/src/components/DataTable/utils/__tests__/rowUtils.test.ts +265 -0
  168. package/src/components/DataTable/utils/errorHandling.ts +52 -460
  169. package/src/components/DataTable/utils/exportUtils.ts +46 -15
  170. package/src/components/DataTable/utils/hierarchicalSorting.ts +50 -3
  171. package/src/components/DataTable/utils/hierarchicalUtils.ts +167 -34
  172. package/src/components/DataTable/utils/index.ts +5 -0
  173. package/src/components/DataTable/utils/rowUtils.ts +68 -0
  174. package/src/components/EventSelector/EventSelector.test.tsx +672 -0
  175. package/src/components/Label/__tests__/Label.test.tsx +434 -0
  176. package/src/components/PublicLayout/__tests__/PublicPageContextChecker.test.tsx +190 -0
  177. package/src/components/PublicLayout/__tests__/PublicPageDebugger.test.tsx +185 -0
  178. package/src/components/PublicLayout/__tests__/PublicPageProvider.test.tsx +313 -0
  179. package/src/components/Select/Select.test.tsx +143 -120
  180. package/src/components/Select/Select.tsx +47 -212
  181. package/src/components/Select/hooks.ts +36 -1
  182. package/src/components/Select/index.ts +2 -1
  183. package/src/hooks/services/__tests__/useServiceHooks.test.tsx +137 -0
  184. package/src/hooks/useSecureDataAccess.test.ts +32 -29
  185. package/src/providers/__tests__/ProviderLifecycle.test.tsx +341 -0
  186. package/src/rbac/hooks/__tests__/usePermissions.integration.test.ts +437 -0
  187. package/src/rbac/hooks/index.ts +2 -0
  188. package/src/rbac/hooks/useResolvedScope.ts +232 -0
  189. package/src/services/__tests__/InactivityService.lifecycle.test.ts +411 -0
  190. package/src/services/__tests__/OrganisationService.pagination.test.ts +375 -0
  191. package/src/types/__tests__/README.md +114 -0
  192. package/src/types/__tests__/validation.test.ts +731 -0
  193. package/src/utils/__tests__/file-reference.test.ts +383 -0
  194. package/src/utils/__tests__/performanceBenchmark.test.ts +175 -0
  195. package/src/utils/appNameResolver.test.ts +54 -0
  196. package/src/validation/__tests__/csrf.unit.test.ts +63 -0
  197. package/src/validation/__tests__/passwordSchema.unit.test.ts +105 -0
  198. package/dist/chunk-2CHATWBF.js.map +0 -1
  199. package/dist/chunk-33PHABLB.js.map +0 -1
  200. package/dist/chunk-CY3AHGO4.js.map +0 -1
  201. package/dist/chunk-TYHR5X4W.js +0 -33
  202. package/dist/chunk-TYHR5X4W.js.map +0 -1
  203. package/dist/chunk-YNUBMSMV.js.map +0 -1
  204. package/dist/eventContext-BBA42P6G.js +0 -14
  205. package/dist/eventContext-BBA42P6G.js.map +0 -1
  206. package/docs/documentation-style-checklist.md +0 -294
  207. package/src/components/DataTable/components/DataTableBody.tsx +0 -488
  208. package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -144
  209. package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -515
  210. package/src/components/DataTable/core/ActionManager.ts +0 -235
  211. package/src/components/DataTable/core/ColumnManager.ts +0 -215
  212. package/src/components/DataTable/core/DataManager.ts +0 -188
  213. package/src/components/DataTable/core/DataTableContext.tsx +0 -181
  214. package/src/components/DataTable/core/LocalDataAdapter.ts +0 -264
  215. package/src/components/DataTable/core/PluginRegistry.ts +0 -229
  216. package/src/components/DataTable/core/StateManager.ts +0 -311
  217. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +0 -634
  218. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +0 -193
  219. package/src/components/DataTable/core/__tests__/DataManager.test.ts +0 -519
  220. package/src/components/DataTable/core/__tests__/StateManager.test.ts +0 -714
  221. package/src/components/DataTable/core/interfaces.ts +0 -338
  222. package/src/components/DataTable/utils/debugTools.ts +0 -583
  223. package/src/components/Select/Select.bug-test.tsx +0 -69
  224. package/src/components/Select/Select.refactored.tsx +0 -497
  225. /package/dist/{DataTable-HWZQGASI.js.map → DataTable-4GAVPIEG.js.map} +0 -0
  226. /package/dist/{chunk-ZTT2AXMX.js.map → chunk-KK73ZB4E.js.map} +0 -0
@@ -1,497 +0,0 @@
1
- /**
2
- * @file Select Component - Refactored SOLID Implementation
3
- * @package @jmruthers/pace-core
4
- * @module Components/Select
5
- * @since 0.4.0
6
- *
7
- * Refactored Select component following SOLID principles:
8
- * - Single Responsibility: Each component has one clear purpose
9
- * - Open/Closed: Easy to extend without modification
10
- * - Liskov Substitution: Components can be substituted
11
- * - Interface Segregation: Small, focused interfaces
12
- * - Dependency Inversion: Depends on abstractions, not concretions
13
- */
14
-
15
- import * as React from "react";
16
- import { Search, X, ChevronDown, Check } from "lucide-react";
17
- import { Button, type ButtonProps } from "../Button/Button";
18
- import { cn } from "../../utils/cn";
19
- import {
20
- useSelectState,
21
- useSelectEvents,
22
- useSelectSearch,
23
- type SelectState,
24
- type SelectActions,
25
- type UseSelectStateProps
26
- } from "./hooks";
27
-
28
- // ============================================================================
29
- // TYPES AND INTERFACES
30
- // ============================================================================
31
-
32
- export interface SelectContextValue extends SelectState {
33
- actions: SelectActions;
34
- }
35
-
36
- export interface SelectProps extends Omit<React.HTMLAttributes<HTMLFormElement>, 'onChange' | 'onKeyDown' | 'onFocus' | 'onBlur'> {
37
- children: React.ReactNode;
38
- className?: string;
39
- }
40
-
41
- export interface SelectTriggerProps extends Omit<ButtonProps, 'onClick' | 'onKeyDown'> {
42
- children: React.ReactNode;
43
- asChild?: boolean;
44
- }
45
-
46
- export interface SelectValueProps {
47
- placeholder?: string;
48
- children?: React.ReactNode;
49
- }
50
-
51
- export interface SelectContentProps {
52
- children: React.ReactNode;
53
- className?: string;
54
- searchable?: boolean;
55
- searchPlaceholder?: string;
56
- maxHeight?: string;
57
- style?: React.CSSProperties;
58
- }
59
-
60
- export interface SelectItemProps {
61
- value: string;
62
- children: React.ReactNode;
63
- disabled?: boolean;
64
- className?: string;
65
- onClick?: (e: React.MouseEvent) => void;
66
- }
67
-
68
- // ============================================================================
69
- // CONTEXT
70
- // ============================================================================
71
-
72
- const SelectContext = React.createContext<SelectContextValue | null>(null);
73
-
74
- const useSelectContext = () => {
75
- const context = React.useContext(SelectContext);
76
- if (!context) {
77
- throw new Error('Select components must be used within a Select');
78
- }
79
- return context;
80
- };
81
-
82
- // ============================================================================
83
- // ROOT COMPONENT
84
- // ============================================================================
85
-
86
- export const Select = React.forwardRef<HTMLFormElement, SelectProps & UseSelectStateProps>(
87
- ({
88
- children,
89
- className,
90
- ...selectProps
91
- }, ref) => {
92
- const internalRef = React.useRef<HTMLFormElement>(null);
93
- const selectRef = React.useMemo(() => {
94
- if (ref && typeof ref === 'object' && 'current' in ref) {
95
- return ref as React.RefObject<HTMLFormElement>;
96
- }
97
- return internalRef;
98
- }, [ref]);
99
-
100
- // Use custom hooks for state management
101
- const { state, actions } = useSelectState(selectProps);
102
- const { isSelecting } = useSelectEvents({ state, actions, selectRef });
103
-
104
- // Find selected text when value changes
105
- React.useEffect(() => {
106
- if (state.value && !state.selectedText) {
107
- // Find the SelectItem with the matching value and extract its text
108
- const selectElement = selectRef.current;
109
- if (selectElement) {
110
- const selectItem = selectElement.querySelector(`[data-value="${state.value}"]`);
111
- if (selectItem) {
112
- const textContent = selectItem.textContent?.trim() || '';
113
- if (textContent) {
114
- actions.setSelectedText(textContent);
115
- }
116
- }
117
- }
118
- }
119
- }, [state.value, state.selectedText, actions, selectRef]);
120
-
121
- const contextValue = React.useMemo<SelectContextValue>(() => ({
122
- ...state,
123
- actions,
124
- }), [state, actions]);
125
-
126
- return (
127
- <form
128
- ref={selectRef}
129
- className={cn("relative", className)}
130
- data-value={state.value}
131
- data-testid="select-root"
132
- >
133
- <SelectContext.Provider value={contextValue}>
134
- {children}
135
- </SelectContext.Provider>
136
- </form>
137
- );
138
- }
139
- );
140
- Select.displayName = "Select";
141
-
142
- // ============================================================================
143
- // TRIGGER COMPONENT
144
- // ============================================================================
145
-
146
- export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerProps>(
147
- ({ children, className, variant = "outline", size = "default", asChild = false, ...props }, ref) => {
148
- const { open, disabled, value, actions } = useSelectContext();
149
-
150
- const handleClick = () => {
151
- actions.setOpen(!open);
152
- };
153
-
154
- const handleKeyDown = (e: React.KeyboardEvent) => {
155
- if (disabled) return;
156
-
157
- switch (e.key) {
158
- case 'Enter':
159
- case ' ':
160
- case 'ArrowDown':
161
- case 'ArrowUp':
162
- e.preventDefault();
163
- actions.setOpen(true);
164
- break;
165
- case 'Escape':
166
- if (open) {
167
- e.preventDefault();
168
- actions.setOpen(false);
169
- }
170
- break;
171
- }
172
- };
173
-
174
- const triggerProps = {
175
- ref,
176
- type: "button" as const,
177
- role: "combobox",
178
- "aria-expanded": open,
179
- "aria-haspopup": "listbox",
180
- disabled,
181
- className: cn(
182
- "!justify-between relative w-full",
183
- "[&_svg]:pointer-events-none",
184
- open && "!rounded-b-none !border-b-0",
185
- className
186
- ),
187
- style: {
188
- ...props.style,
189
- overflow: 'hidden',
190
- textOverflow: 'ellipsis',
191
- whiteSpace: 'nowrap'
192
- },
193
- onClick: handleClick,
194
- onKeyDown: handleKeyDown,
195
- "data-testid": "select-trigger",
196
- "data-value": value,
197
- ...props
198
- };
199
-
200
- if (asChild) {
201
- const childChildren = React.Children.toArray((children as React.ReactElement).props.children);
202
- const hasChevron = childChildren.some(child =>
203
- React.isValidElement(child) &&
204
- (child.type === ChevronDown ||
205
- (child.type === 'svg' && child.props['data-testid'] === 'chevron-down') ||
206
- (typeof child === 'object' && 'type' in child && typeof child.type === 'function' && child.type.name === 'ChevronDown'))
207
- );
208
-
209
- return React.cloneElement(children as React.ReactElement, {
210
- ...triggerProps,
211
- children: hasChevron ? childChildren : [
212
- ...childChildren,
213
- <ChevronDown
214
- key="chevron-down"
215
- className={cn(
216
- "h-4 w-4 opacity-50 transition-transform pointer-events-none float-right",
217
- open && "rotate-180"
218
- )}
219
- />
220
- ]
221
- });
222
- }
223
-
224
- return (
225
- <Button
226
- ref={ref}
227
- type="button"
228
- role="combobox"
229
- aria-expanded={open}
230
- aria-haspopup="listbox"
231
- disabled={disabled}
232
- variant={variant}
233
- size={size}
234
- className={cn(
235
- "!justify-between relative w-full",
236
- "[&_svg]:pointer-events-none",
237
- open && "!rounded-b-none !border-b-0",
238
- className
239
- )}
240
- style={{
241
- ...props.style,
242
- overflow: 'hidden',
243
- textOverflow: 'ellipsis',
244
- whiteSpace: 'nowrap'
245
- }}
246
- onClick={handleClick}
247
- onKeyDown={handleKeyDown}
248
- data-testid="select-trigger"
249
- data-value={value}
250
- {...props}
251
- >
252
- {children}
253
- <ChevronDown
254
- className={cn(
255
- "h-4 w-4 opacity-50 transition-transform pointer-events-none float-right",
256
- open && "rotate-180"
257
- )}
258
- />
259
- </Button>
260
- );
261
- }
262
- );
263
- SelectTrigger.displayName = "SelectTrigger";
264
-
265
- // ============================================================================
266
- // VALUE COMPONENT
267
- // ============================================================================
268
-
269
- export const SelectValue = React.forwardRef<HTMLSpanElement, SelectValueProps>(
270
- ({ placeholder = "Select an option...", children }, ref) => {
271
- const { selectedText } = useSelectContext();
272
-
273
- return (
274
- <span ref={ref} data-testid="select-value">
275
- {children || (selectedText ? selectedText : placeholder)}
276
- </span>
277
- );
278
- }
279
- );
280
- SelectValue.displayName = "SelectValue";
281
-
282
- // ============================================================================
283
- // CONTENT COMPONENT
284
- // ============================================================================
285
-
286
- export const SelectContent = React.forwardRef<HTMLUListElement, SelectContentProps>(
287
- ({
288
- children,
289
- className,
290
- searchable = false,
291
- searchPlaceholder = "Search...",
292
- maxHeight = "20rem",
293
- style
294
- }, ref) => {
295
- const { open, actions } = useSelectContext();
296
- const { searchTerm, setSearchTerm, filteredChildren, searchInputRef } = useSelectSearch({
297
- children,
298
- searchable,
299
- });
300
-
301
- // Focus search input when dropdown opens
302
- React.useEffect(() => {
303
- if (open && searchable && searchInputRef.current) {
304
- searchInputRef.current.focus();
305
- }
306
- }, [open, searchable]);
307
-
308
- if (!open) {
309
- return null;
310
- }
311
-
312
- return (
313
- <ul
314
- ref={ref}
315
- className={cn(
316
- "absolute z-[99999] w-full overflow-y-auto rounded-b-md border border-t-0 border-main-300 bg-main-50 shadow-lg",
317
- "list-none p-0 m-0",
318
- className
319
- )}
320
- style={{
321
- top: '100%',
322
- left: 0,
323
- right: 0,
324
- maxHeight,
325
- position: 'absolute',
326
- zIndex: 99999,
327
- ...style
328
- }}
329
- data-testid="select-content"
330
- role="listbox"
331
- >
332
- {searchable && (
333
- <div className="p-2 border-b border-main-200">
334
- <div className="relative">
335
- <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-main-400" />
336
- <input
337
- ref={searchInputRef}
338
- type="text"
339
- placeholder={searchPlaceholder}
340
- value={searchTerm}
341
- onChange={(e) => setSearchTerm(e.target.value)}
342
- onKeyDown={(e) => {
343
- if (e.key === 'Escape') {
344
- e.preventDefault();
345
- setSearchTerm('');
346
- }
347
- }}
348
- className="w-full pl-8 pr-8 py-1 text-sm border border-main-200 rounded focus:outline-none focus:ring-2 focus:ring-main-500"
349
- data-testid="select-search-input"
350
- aria-label="Search options"
351
- />
352
- {searchTerm && (
353
- <button
354
- type="button"
355
- onClick={() => setSearchTerm('')}
356
- className="absolute right-2 top-1/2 transform -translate-y-1/2 text-main-400 hover:text-main-600"
357
- data-testid="select-clear-search"
358
- aria-label="Clear search"
359
- >
360
- <X className="h-4 w-4" />
361
- </button>
362
- )}
363
- </div>
364
- </div>
365
- )}
366
- {filteredChildren}
367
- </ul>
368
- );
369
- }
370
- );
371
- SelectContent.displayName = "SelectContent";
372
-
373
- // ============================================================================
374
- // ITEM COMPONENT
375
- // ============================================================================
376
-
377
- export const SelectItem = React.forwardRef<HTMLLIElement, SelectItemProps>(
378
- ({ value, children, disabled = false, className, onClick }, ref) => {
379
- const { value: selectedValue, actions } = useSelectContext();
380
- const isSelected = selectedValue === value;
381
-
382
- // Extract text content from children for display
383
- const getTextContent = (children: React.ReactNode): string => {
384
- if (typeof children === 'string') return children;
385
- if (typeof children === 'number') return children.toString();
386
- if (React.isValidElement(children) && children.props.children) {
387
- return getTextContent(children.props.children);
388
- }
389
- if (Array.isArray(children)) {
390
- return children.map(getTextContent).join('');
391
- }
392
- return '';
393
- };
394
-
395
- const itemText = getTextContent(children);
396
-
397
- const handleMouseDown = (e: React.MouseEvent) => {
398
- if (!disabled) {
399
- const event = new CustomEvent('selectItemMouseDown', { detail: { value } });
400
- document.dispatchEvent(event);
401
- }
402
- };
403
-
404
- const handleClick = (e: React.MouseEvent) => {
405
- if (!disabled) {
406
- if (onClick) {
407
- onClick(e);
408
- }
409
- actions.setValue(value, itemText);
410
- actions.setOpen(false);
411
- }
412
- };
413
-
414
- const handleKeyDown = (e: React.KeyboardEvent) => {
415
- if (disabled) return;
416
-
417
- switch (e.key) {
418
- case 'Enter':
419
- case ' ':
420
- e.preventDefault();
421
- if (onClick) {
422
- onClick(e as any);
423
- }
424
- actions.setValue(value, itemText);
425
- actions.setOpen(false);
426
- break;
427
- }
428
- };
429
-
430
- return (
431
- <li
432
- ref={ref}
433
- data-value={value}
434
- className={cn(
435
- "relative flex cursor-pointer select-none items-start rounded-sm px-2 py-1.5 text-sm outline-none",
436
- "hover:bg-main-100 focus:bg-main-100",
437
- "break-words min-w-0",
438
- isSelected && "bg-main-100 text-main-900",
439
- disabled && "pointer-events-none opacity-50",
440
- className
441
- )}
442
- onMouseDown={handleMouseDown}
443
- onClick={handleClick}
444
- onKeyDown={handleKeyDown}
445
- data-testid="select-item"
446
- data-disabled={disabled ? "true" : undefined}
447
- role="option"
448
- aria-selected={isSelected}
449
- >
450
- {children}
451
- {isSelected && (
452
- <Check className="absolute right-2 h-4 w-4 flex-shrink-0 mt-0.5" />
453
- )}
454
- </li>
455
- );
456
- }
457
- );
458
- SelectItem.displayName = "SelectItem";
459
-
460
- // ============================================================================
461
- // ADDITIONAL COMPONENTS (for backward compatibility)
462
- // ============================================================================
463
-
464
- export const SelectGroup = React.forwardRef<HTMLDivElement, { children: React.ReactNode; className?: string }>(
465
- ({ children, className }, ref) => {
466
- return (
467
- <div ref={ref} className={cn("p-1", className)} data-testid="select-group">
468
- {children}
469
- </div>
470
- );
471
- }
472
- );
473
- SelectGroup.displayName = "SelectGroup";
474
-
475
- export const SelectLabel = React.forwardRef<HTMLDivElement, { children: React.ReactNode; className?: string }>(
476
- ({ children, className }, ref) => {
477
- return (
478
- <div ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} data-testid="select-label">
479
- {children}
480
- </div>
481
- );
482
- }
483
- );
484
- SelectLabel.displayName = "SelectLabel";
485
-
486
- export const SelectSeparator = React.forwardRef<HTMLDivElement, { className?: string }>(
487
- ({ className }, ref) => {
488
- return (
489
- <div
490
- ref={ref}
491
- className={cn("my-1 h-px bg-sec-200", className)}
492
- data-testid="select-separator"
493
- />
494
- );
495
- }
496
- );
497
- SelectSeparator.displayName = "SelectSeparator";