@jmruthers/pace-core 0.5.186 → 0.5.187

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 (284) hide show
  1. package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
  2. package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
  3. package/dist/{PublicPageProvider-DIzEzwKl.d.ts → PublicPageProvider-DrLDztHt.d.ts} +211 -106
  4. package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
  5. package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
  6. package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
  7. package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
  8. package/dist/chunk-3IC5WCMO.js.map +1 -0
  9. package/dist/{chunk-OALXJH4Y.js → chunk-3NFNJOO7.js} +8 -8
  10. package/dist/chunk-3NFNJOO7.js.map +1 -0
  11. package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
  12. package/dist/chunk-63FOKYGO.js.map +1 -0
  13. package/dist/{chunk-TC7D3CR3.js → chunk-C4OYJOV4.js} +556 -101
  14. package/dist/chunk-C4OYJOV4.js.map +1 -0
  15. package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
  16. package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
  17. package/dist/chunk-HEHYGYOX.js.map +1 -0
  18. package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
  19. package/dist/{chunk-HDCUMOOI.js → chunk-LBBUPSSC.js} +792 -559
  20. package/dist/chunk-LBBUPSSC.js.map +1 -0
  21. package/dist/{chunk-UQWSHFVX.js → chunk-SAUPYVLF.js} +1 -1
  22. package/dist/{chunk-UQWSHFVX.js.map → chunk-SAUPYVLF.js.map} +1 -1
  23. package/dist/{chunk-GRIQLQ52.js → chunk-T6ZJVI3A.js} +27 -23
  24. package/dist/chunk-T6ZJVI3A.js.map +1 -0
  25. package/dist/{chunk-DAGICKHT.js → chunk-ULX5FYEM.js} +3 -3
  26. package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
  27. package/dist/chunk-WK2Y6TGA.js.map +1 -0
  28. package/dist/chunk-YHCN776L.js +447 -0
  29. package/dist/chunk-YHCN776L.js.map +1 -0
  30. package/dist/components.d.ts +4 -4
  31. package/dist/components.js +12 -10
  32. package/dist/components.js.map +1 -1
  33. package/dist/{file-reference-PRTSLxKx.d.ts → file-reference-D037xOFK.d.ts} +0 -1
  34. package/dist/hooks.d.ts +221 -6
  35. package/dist/hooks.js +146 -49
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +24 -9
  38. package/dist/index.js +62 -28
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.js +1 -1
  41. package/dist/rbac/index.d.ts +124 -7
  42. package/dist/rbac/index.js +27 -7
  43. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  44. package/dist/types.d.ts +1 -1
  45. package/dist/types.js +1 -1
  46. package/dist/{usePublicRouteParams-D71QLlg4.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +2 -2
  47. package/dist/utils.d.ts +213 -3
  48. package/dist/utils.js +22 -2
  49. package/dist/utils.js.map +1 -1
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/Logger.md +1 -1
  54. package/docs/api/classes/MissingUserContextError.md +1 -1
  55. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  56. package/docs/api/classes/PermissionDeniedError.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +21 -17
  58. package/docs/api/classes/RBACCache.md +31 -23
  59. package/docs/api/classes/RBACEngine.md +5 -5
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/enums/LogLevel.md +1 -1
  66. package/docs/api/enums/RBACErrorCode.md +1 -1
  67. package/docs/api/enums/RPCFunction.md +1 -1
  68. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  69. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  70. package/docs/api/interfaces/AggregateConfig.md +1 -1
  71. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  72. package/docs/api/interfaces/BadgeProps.md +1 -1
  73. package/docs/api/interfaces/ButtonProps.md +1 -1
  74. package/docs/api/interfaces/CalendarProps.md +1 -1
  75. package/docs/api/interfaces/CardProps.md +1 -1
  76. package/docs/api/interfaces/ColorPalette.md +1 -1
  77. package/docs/api/interfaces/ColorShade.md +1 -1
  78. package/docs/api/interfaces/ComplianceResult.md +1 -1
  79. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  80. package/docs/api/interfaces/DataRecord.md +1 -1
  81. package/docs/api/interfaces/DataTableAction.md +1 -1
  82. package/docs/api/interfaces/DataTableColumn.md +1 -1
  83. package/docs/api/interfaces/DataTableProps.md +1 -1
  84. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  85. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  86. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  87. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  88. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  89. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  90. package/docs/api/interfaces/ExportColumn.md +1 -1
  91. package/docs/api/interfaces/ExportOptions.md +1 -1
  92. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  93. package/docs/api/interfaces/FileMetadata.md +1 -1
  94. package/docs/api/interfaces/FileReference.md +1 -1
  95. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  96. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  97. package/docs/api/interfaces/FileUploadProps.md +1 -1
  98. package/docs/api/interfaces/FooterProps.md +1 -1
  99. package/docs/api/interfaces/FormFieldProps.md +1 -1
  100. package/docs/api/interfaces/FormProps.md +1 -1
  101. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  102. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  103. package/docs/api/interfaces/InputProps.md +1 -1
  104. package/docs/api/interfaces/LabelProps.md +1 -1
  105. package/docs/api/interfaces/LoggerConfig.md +1 -1
  106. package/docs/api/interfaces/LoginFormProps.md +1 -1
  107. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  108. package/docs/api/interfaces/NavigationContextType.md +1 -1
  109. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  110. package/docs/api/interfaces/NavigationItem.md +1 -1
  111. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  112. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  113. package/docs/api/interfaces/Organisation.md +1 -1
  114. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  115. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  116. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  117. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  118. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  119. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  120. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  121. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  122. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  123. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  124. package/docs/api/interfaces/PaletteData.md +1 -1
  125. package/docs/api/interfaces/ParsedAddress.md +120 -0
  126. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  127. package/docs/api/interfaces/ProgressProps.md +1 -1
  128. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  129. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  130. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  131. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  132. package/docs/api/interfaces/QuickFix.md +1 -1
  133. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  134. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  135. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  136. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  137. package/docs/api/interfaces/RBACConfig.md +26 -3
  138. package/docs/api/interfaces/RBACContext.md +1 -1
  139. package/docs/api/interfaces/RBACLogger.md +5 -5
  140. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  141. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  142. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  144. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  146. package/docs/api/interfaces/RBACResult.md +1 -1
  147. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  148. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  153. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  154. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  155. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  156. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  157. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  158. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  159. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  160. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  161. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  162. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  163. package/docs/api/interfaces/RouteConfig.md +1 -1
  164. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  165. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  166. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  167. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  168. package/docs/api/interfaces/SetupIssue.md +1 -1
  169. package/docs/api/interfaces/StorageConfig.md +1 -1
  170. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  171. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  172. package/docs/api/interfaces/StorageListOptions.md +1 -1
  173. package/docs/api/interfaces/StorageListResult.md +1 -1
  174. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  175. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  176. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  177. package/docs/api/interfaces/StyleImport.md +1 -1
  178. package/docs/api/interfaces/SwitchProps.md +1 -1
  179. package/docs/api/interfaces/TabsContentProps.md +1 -1
  180. package/docs/api/interfaces/TabsListProps.md +1 -1
  181. package/docs/api/interfaces/TabsProps.md +1 -1
  182. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  183. package/docs/api/interfaces/TextareaProps.md +1 -1
  184. package/docs/api/interfaces/ToastActionElement.md +1 -1
  185. package/docs/api/interfaces/ToastProps.md +1 -1
  186. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  187. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  188. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  189. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  190. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  191. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  192. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  193. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  195. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  199. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  200. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  201. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  202. package/docs/api/interfaces/UserEventAccess.md +1 -1
  203. package/docs/api/interfaces/UserMenuProps.md +1 -1
  204. package/docs/api/interfaces/UserProfile.md +1 -1
  205. package/docs/api/modules.md +318 -59
  206. package/docs/best-practices/performance.md +11 -0
  207. package/docs/implementation-guides/file-upload-storage.md +29 -0
  208. package/docs/rbac/README.md +2 -1
  209. package/docs/rbac/api-reference.md +11 -0
  210. package/docs/rbac/performance.md +320 -0
  211. package/docs/standards/01-architecture-standard.md +5 -0
  212. package/docs/standards/05-security-standard.md +12 -0
  213. package/package.json +1 -1
  214. package/src/components/AddressField/AddressField.test.tsx +411 -0
  215. package/src/components/AddressField/AddressField.tsx +323 -0
  216. package/src/components/AddressField/README.md +336 -0
  217. package/src/components/AddressField/index.ts +10 -0
  218. package/src/components/AddressField/types.ts +65 -0
  219. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
  220. package/src/components/FileDisplay/FileDisplay.tsx +28 -1
  221. package/src/components/index.ts +2 -0
  222. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  223. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  224. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  225. package/src/hooks/index.ts +6 -0
  226. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  227. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  228. package/src/hooks/useAddressAutocomplete.ts +268 -0
  229. package/src/hooks/useFileDisplay.ts +3 -15
  230. package/src/hooks/useFileReference.test.ts +20 -3
  231. package/src/hooks/useFileReference.ts +3 -24
  232. package/src/hooks/useFileUrlCache.ts +246 -0
  233. package/src/hooks/useInactivityTracker.ts +31 -20
  234. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  235. package/src/hooks/useOrganisationSecurity.ts +3 -3
  236. package/src/hooks/useQueryCache.ts +315 -0
  237. package/src/index.ts +2 -0
  238. package/src/providers/services/EventServiceProvider.tsx +4 -1
  239. package/src/rbac/api.test.ts +21 -6
  240. package/src/rbac/api.ts +32 -11
  241. package/src/rbac/audit-batched.ts +223 -0
  242. package/src/rbac/audit-enhanced.ts +2 -2
  243. package/src/rbac/audit.test.ts +6 -5
  244. package/src/rbac/audit.ts +34 -6
  245. package/src/rbac/cache-invalidation.ts +63 -12
  246. package/src/rbac/cache.test.ts +2 -2
  247. package/src/rbac/cache.ts +61 -14
  248. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  249. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  250. package/src/rbac/config.ts +9 -0
  251. package/src/rbac/engine.ts +2 -21
  252. package/src/rbac/hooks/usePermissions.ts +21 -5
  253. package/src/rbac/index.ts +19 -0
  254. package/src/rbac/performance.ts +210 -0
  255. package/src/rbac/request-deduplication.ts +87 -0
  256. package/src/rbac/utils/deep-equal.ts +93 -0
  257. package/src/types/file-reference.ts +0 -1
  258. package/src/utils/file-reference/__tests__/file-reference.test.ts +31 -4
  259. package/src/utils/file-reference/index.ts +44 -15
  260. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  261. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  262. package/src/utils/google-places/index.ts +26 -0
  263. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  264. package/src/utils/google-places/types.ts +94 -0
  265. package/src/utils/index.ts +23 -0
  266. package/src/utils/request-deduplication.ts +165 -0
  267. package/src/utils/storage/helpers.ts +143 -4
  268. package/dist/chunk-445GEP27.js.map +0 -1
  269. package/dist/chunk-FMUCXFII.js +0 -76
  270. package/dist/chunk-FMUCXFII.js.map +0 -1
  271. package/dist/chunk-FSFQFJCU.js.map +0 -1
  272. package/dist/chunk-FXFJRTKI.js.map +0 -1
  273. package/dist/chunk-GRIQLQ52.js.map +0 -1
  274. package/dist/chunk-HDCUMOOI.js.map +0 -1
  275. package/dist/chunk-OALXJH4Y.js.map +0 -1
  276. package/dist/chunk-TC7D3CR3.js.map +0 -1
  277. package/dist/chunk-U6WNSFX5.js.map +0 -1
  278. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
  279. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
  280. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  281. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  282. /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
  283. /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
  284. /package/dist/{chunk-DAGICKHT.js.map → chunk-ULX5FYEM.js.map} +0 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * @file AddressField Component
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/AddressField
5
+ * @since 0.1.0
6
+ *
7
+ * Address input field with Google Places API autocomplete.
8
+ * Provides address suggestions, keyboard navigation, and accessibility.
9
+ *
10
+ * Features:
11
+ * - Google Places API autocomplete integration
12
+ * - Debounced input with caching
13
+ * - Keyboard navigation (Arrow keys, Enter, Escape)
14
+ * - Accessible ARIA attributes
15
+ * - Loading and error states
16
+ * - place_id storage for later retrieval
17
+ */
18
+
19
+ import * as React from 'react';
20
+ import { Input } from '../Input/Input';
21
+ import { LoadingSpinner } from '../LoadingSpinner';
22
+ import { cn } from '../../utils/core/cn';
23
+ import { useAddressAutocomplete } from '../../hooks/useAddressAutocomplete';
24
+ import type { AddressFieldProps, AddressFieldRef } from './types';
25
+ import type { ParsedAddress } from '../../utils/google-places';
26
+
27
+ /**
28
+ * AddressField component
29
+ *
30
+ * A production-ready address input field with Google Places API autocomplete.
31
+ * Returns structured address data including place_id for later retrieval.
32
+ *
33
+ * @param props - AddressField configuration
34
+ * @param ref - Forwarded ref for imperative access
35
+ * @returns JSX.Element - The rendered address field
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * <AddressField
40
+ * apiKey={apiKey}
41
+ * onChange={(address) => {
42
+ * // address includes place_id, full_address, lat, lng, etc.
43
+ * console.log(address.place_id);
44
+ * }}
45
+ * placeholder="Enter your address"
46
+ * />
47
+ * ```
48
+ */
49
+ const AddressField = React.forwardRef<HTMLInputElement, AddressFieldProps>(
50
+ (
51
+ {
52
+ apiKey,
53
+ value: controlledValue,
54
+ defaultValue = '',
55
+ onChange,
56
+ onInputChange,
57
+ placeholder = 'Enter address',
58
+ error,
59
+ disabled,
60
+ className,
61
+ size = 'md',
62
+ variant = 'default',
63
+ autocompleteOptions,
64
+ debounceDelay,
65
+ cacheEnabled = true,
66
+ cacheTTL,
67
+ ...props
68
+ },
69
+ ref
70
+ ) => {
71
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
72
+ const [isOpen, setIsOpen] = React.useState(false);
73
+ const [selectedIndex, setSelectedIndex] = React.useState(-1);
74
+ const [inputFocused, setInputFocused] = React.useState(false);
75
+
76
+ const inputRef = React.useRef<HTMLInputElement>(null);
77
+ const suggestionsRef = React.useRef<HTMLUListElement>(null);
78
+ const containerRef = React.useRef<HTMLDivElement>(null);
79
+
80
+ // Use controlled or uncontrolled value
81
+ const value = controlledValue !== undefined ? controlledValue : internalValue;
82
+
83
+ // Use autocomplete hook
84
+ const { suggestions, isLoading, error: autocompleteError, selectAddress, clearSuggestions } =
85
+ useAddressAutocomplete(apiKey, value, {
86
+ autocompleteOptions,
87
+ debounceDelay,
88
+ cacheEnabled,
89
+ cacheTTL,
90
+ });
91
+
92
+ // Update suggestions visibility
93
+ React.useEffect(() => {
94
+ if (suggestions.length > 0 && inputFocused && value.trim()) {
95
+ setIsOpen(true);
96
+ } else if (suggestions.length === 0 || !value.trim()) {
97
+ setIsOpen(false);
98
+ }
99
+ }, [suggestions, inputFocused, value]);
100
+
101
+ // Handle input change
102
+ const handleInputChange = React.useCallback(
103
+ (e: React.ChangeEvent<HTMLInputElement>) => {
104
+ const newValue = e.target.value;
105
+ if (controlledValue === undefined) {
106
+ setInternalValue(newValue);
107
+ }
108
+ onInputChange?.(newValue);
109
+ setSelectedIndex(-1);
110
+ if (!newValue.trim()) {
111
+ onChange?.(null);
112
+ clearSuggestions();
113
+ }
114
+ },
115
+ [controlledValue, onInputChange, onChange, clearSuggestions]
116
+ );
117
+
118
+ // Handle address selection
119
+ const handleSelectAddress = React.useCallback(
120
+ async (placeId: string) => {
121
+ setIsOpen(false);
122
+ setSelectedIndex(-1);
123
+ const address = await selectAddress(placeId);
124
+ if (address) {
125
+ // Update input with formatted address
126
+ const displayValue = address.full_address || '';
127
+ if (controlledValue === undefined) {
128
+ setInternalValue(displayValue);
129
+ }
130
+ onInputChange?.(displayValue);
131
+ onChange?.(address);
132
+ }
133
+ inputRef.current?.blur();
134
+ },
135
+ [selectAddress, onChange, onInputChange, controlledValue]
136
+ );
137
+
138
+ // Handle keyboard navigation
139
+ const handleKeyDown = React.useCallback(
140
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
141
+ if (!isOpen || suggestions.length === 0) {
142
+ if (e.key === 'Escape') {
143
+ setIsOpen(false);
144
+ inputRef.current?.blur();
145
+ }
146
+ return;
147
+ }
148
+
149
+ switch (e.key) {
150
+ case 'ArrowDown':
151
+ e.preventDefault();
152
+ setSelectedIndex((prev) => (prev < suggestions.length - 1 ? prev + 1 : prev));
153
+ break;
154
+ case 'ArrowUp':
155
+ e.preventDefault();
156
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1));
157
+ break;
158
+ case 'Enter':
159
+ e.preventDefault();
160
+ if (selectedIndex >= 0 && selectedIndex < suggestions.length) {
161
+ handleSelectAddress(suggestions[selectedIndex].place_id);
162
+ }
163
+ break;
164
+ case 'Escape':
165
+ e.preventDefault();
166
+ setIsOpen(false);
167
+ setSelectedIndex(-1);
168
+ inputRef.current?.blur();
169
+ break;
170
+ case 'Tab':
171
+ setIsOpen(false);
172
+ setSelectedIndex(-1);
173
+ break;
174
+ }
175
+ },
176
+ [isOpen, suggestions, selectedIndex, handleSelectAddress]
177
+ );
178
+
179
+ // Handle focus
180
+ const handleFocus = React.useCallback(() => {
181
+ setInputFocused(true);
182
+ if (suggestions.length > 0 && value.trim()) {
183
+ setIsOpen(true);
184
+ }
185
+ }, [suggestions, value]);
186
+
187
+ // Handle blur
188
+ const handleBlur = React.useCallback(
189
+ (e: React.FocusEvent<HTMLInputElement>) => {
190
+ // Delay to allow click events on suggestions
191
+ setTimeout(() => {
192
+ if (!containerRef.current?.contains(document.activeElement)) {
193
+ setInputFocused(false);
194
+ setIsOpen(false);
195
+ setSelectedIndex(-1);
196
+ }
197
+ }, 200);
198
+ },
199
+ []
200
+ );
201
+
202
+ // Click outside handler
203
+ React.useEffect(() => {
204
+ const handleClickOutside = (event: MouseEvent) => {
205
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
206
+ setIsOpen(false);
207
+ setSelectedIndex(-1);
208
+ }
209
+ };
210
+
211
+ if (isOpen) {
212
+ document.addEventListener('mousedown', handleClickOutside);
213
+ return () => {
214
+ document.removeEventListener('mousedown', handleClickOutside);
215
+ };
216
+ }
217
+ }, [isOpen]);
218
+
219
+ // Scroll selected item into view
220
+ React.useEffect(() => {
221
+ if (selectedIndex >= 0 && suggestionsRef.current) {
222
+ const selectedItem = suggestionsRef.current.children[selectedIndex] as HTMLElement;
223
+ if (selectedItem && typeof selectedItem.scrollIntoView === 'function') {
224
+ selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
225
+ }
226
+ }
227
+ }, [selectedIndex]);
228
+
229
+ // Combine refs
230
+ React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);
231
+
232
+ const suggestionsId = React.useId();
233
+ const hasError = error || !!autocompleteError;
234
+
235
+ return (
236
+ <div ref={containerRef} className={cn('relative w-full', className)}>
237
+ <div className="relative">
238
+ <Input
239
+ ref={inputRef}
240
+ type="text"
241
+ value={value}
242
+ onChange={handleInputChange}
243
+ onKeyDown={handleKeyDown}
244
+ onFocus={handleFocus}
245
+ onBlur={handleBlur}
246
+ placeholder={placeholder}
247
+ disabled={disabled}
248
+ error={hasError}
249
+ size={size}
250
+ variant={variant}
251
+ role="combobox"
252
+ aria-expanded={isOpen}
253
+ aria-autocomplete="list"
254
+ aria-controls={suggestionsId}
255
+ aria-haspopup="listbox"
256
+ aria-activedescendant={
257
+ selectedIndex >= 0 ? `${suggestionsId}-item-${selectedIndex}` : undefined
258
+ }
259
+ {...props}
260
+ />
261
+ {isLoading && (
262
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
263
+ <LoadingSpinner size="sm" />
264
+ </div>
265
+ )}
266
+ </div>
267
+
268
+ {isOpen && suggestions.length > 0 && (
269
+ <ul
270
+ ref={suggestionsRef}
271
+ id={suggestionsId}
272
+ role="listbox"
273
+ className={cn(
274
+ 'absolute z-[99999] w-full mt-1 max-h-60 overflow-y-auto',
275
+ 'border border-main-300 bg-main-50 shadow-lg rounded-md',
276
+ 'list-none p-0 m-0'
277
+ )}
278
+ data-testid="address-suggestions"
279
+ >
280
+ {suggestions.map((suggestion, index) => (
281
+ <li
282
+ key={suggestion.place_id}
283
+ id={`${suggestionsId}-item-${index}`}
284
+ role="option"
285
+ aria-selected={selectedIndex === index}
286
+ className={cn(
287
+ 'px-3 py-2 cursor-pointer text-sm',
288
+ 'hover:bg-main-100 focus:bg-main-100',
289
+ 'border-b border-main-200 last:border-b-0',
290
+ selectedIndex === index && 'bg-main-100'
291
+ )}
292
+ onClick={() => handleSelectAddress(suggestion.place_id)}
293
+ onMouseEnter={() => setSelectedIndex(index)}
294
+ data-testid={`address-suggestion-${index}`}
295
+ >
296
+ <div className="font-medium text-main-900">
297
+ {suggestion.structured_formatting?.main_text || suggestion.description}
298
+ </div>
299
+ {suggestion.structured_formatting?.secondary_text && (
300
+ <div className="text-xs text-main-600 mt-0.5">
301
+ {suggestion.structured_formatting.secondary_text}
302
+ </div>
303
+ )}
304
+ </li>
305
+ ))}
306
+ </ul>
307
+ )}
308
+
309
+ {autocompleteError && (
310
+ <p className="mt-1 text-sm text-destructive" role="alert">
311
+ {autocompleteError.message}
312
+ </p>
313
+ )}
314
+ </div>
315
+ );
316
+ }
317
+ );
318
+
319
+ AddressField.displayName = 'AddressField';
320
+
321
+ export { AddressField };
322
+ export type { AddressFieldProps, AddressFieldRef, ParsedAddress } from './types';
323
+
@@ -0,0 +1,336 @@
1
+ # AddressField Component
2
+
3
+ A production-ready address input field with Google Places API autocomplete integration. Provides address suggestions, keyboard navigation, caching for cost optimization, and returns structured address data including `place_id` for later retrieval.
4
+
5
+ ## Features
6
+
7
+ - **Google Places API Integration**: Real-time address autocomplete suggestions
8
+ - **Cost Optimization**: Built-in caching (1hr for autocomplete, 24hr for place details)
9
+ - **place_id Storage**: Always returns `place_id` for efficient address retrieval
10
+ - **Keyboard Navigation**: Full keyboard support (Arrow keys, Enter, Escape)
11
+ - **Accessibility**: WCAG compliant with proper ARIA attributes
12
+ - **Error Handling**: Graceful error handling with user-friendly messages
13
+ - **Loading States**: Visual feedback during API calls
14
+ - **TypeScript**: Fully typed with comprehensive type definitions
15
+
16
+ ## Installation
17
+
18
+ The component is part of `@jmruthers/pace-core`. No additional installation required.
19
+
20
+ ## Setup
21
+
22
+ ### Google Cloud Console Configuration
23
+
24
+ 1. **Enable the Places API** in your Google Cloud Console
25
+ 2. **Create an API Key** with the following restrictions:
26
+ - **Application restrictions**: HTTP referrers (web sites)
27
+ - **Website restrictions**: Add your domain(s) and `http://localhost:*` for local development
28
+ - Example: `http://localhost:8091/*`
29
+ - Example: `https://yourdomain.com/*`
30
+ 3. **API restrictions**: Restrict to "Places API" only (recommended for security)
31
+
32
+ ### Environment Variable
33
+
34
+ Set your API key in your `.env` file:
35
+
36
+ ```bash
37
+ VITE_GOOGLE_PLACES_API_KEY=your-api-key-here
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Basic Example
43
+
44
+ ```tsx
45
+ import { AddressField } from '@jmruthers/pace-core';
46
+
47
+ function MyForm() {
48
+ const [address, setAddress] = useState(null);
49
+ const apiKey = import.meta.env.VITE_GOOGLE_PLACES_API_KEY;
50
+
51
+ return (
52
+ <AddressField
53
+ apiKey={apiKey}
54
+ onChange={(parsedAddress) => {
55
+ setAddress(parsedAddress);
56
+ // parsedAddress includes place_id, full_address, lat, lng, etc.
57
+ console.log('Selected place_id:', parsedAddress.place_id);
58
+ }}
59
+ placeholder="Enter your address"
60
+ />
61
+ );
62
+ }
63
+ ```
64
+
65
+ ### Controlled Input
66
+
67
+ ```tsx
68
+ function ControlledExample() {
69
+ const [inputValue, setInputValue] = useState('');
70
+ const [address, setAddress] = useState(null);
71
+
72
+ return (
73
+ <AddressField
74
+ apiKey={apiKey}
75
+ value={inputValue}
76
+ onInputChange={setInputValue}
77
+ onChange={setAddress}
78
+ />
79
+ );
80
+ }
81
+ ```
82
+
83
+ ### With Country Restriction
84
+
85
+ ```tsx
86
+ <AddressField
87
+ apiKey={apiKey}
88
+ onChange={handleAddressChange}
89
+ autocompleteOptions={{
90
+ components: 'country:au', // Restrict to Australia
91
+ }}
92
+ />
93
+ ```
94
+
95
+ ### With Custom Cache Settings
96
+
97
+ ```tsx
98
+ <AddressField
99
+ apiKey={apiKey}
100
+ onChange={handleAddressChange}
101
+ cacheEnabled={true}
102
+ cacheTTL={{
103
+ autocomplete: 1800, // 30 minutes
104
+ placeDetails: 43200, // 12 hours
105
+ }}
106
+ />
107
+ ```
108
+
109
+ ### Retrieving Address from Stored place_id
110
+
111
+ If you've stored a `place_id` in your database, you can retrieve the full address later:
112
+
113
+ ```tsx
114
+ import { getAddressByPlaceId } from '@jmruthers/pace-core/utils/google-places';
115
+
116
+ // Later, retrieve address from stored place_id
117
+ const address = await getAddressByPlaceId(storedPlaceId, apiKey);
118
+ if (address) {
119
+ console.log(address.full_address);
120
+ console.log(address.lat, address.lng);
121
+ }
122
+ ```
123
+
124
+ ## Props
125
+
126
+ ### AddressFieldProps
127
+
128
+ | Prop | Type | Default | Description |
129
+ |------|------|---------|-------------|
130
+ | `apiKey` | `string` | **required** | Google Places API key |
131
+ | `value` | `string` | - | Controlled input value |
132
+ | `defaultValue` | `string` | `''` | Uncontrolled default value |
133
+ | `onChange` | `(address: ParsedAddress \| null) => void` | - | Callback when address is selected |
134
+ | `onInputChange` | `(value: string) => void` | - | Callback when input value changes |
135
+ | `placeholder` | `string` | `'Enter address'` | Placeholder text |
136
+ | `error` | `boolean` | `false` | Error state styling |
137
+ | `disabled` | `boolean` | `false` | Disabled state |
138
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Input size |
139
+ | `variant` | `'default' \| 'destructive'` | `'default'` | Input variant |
140
+ | `autocompleteOptions` | `AutocompleteOptions` | - | Google Places API options |
141
+ | `debounceDelay` | `number` | `300` | Debounce delay in milliseconds |
142
+ | `cacheEnabled` | `boolean` | `true` | Enable caching |
143
+ | `cacheTTL` | `{ autocomplete?: number; placeDetails?: number }` | `{ autocomplete: 3600, placeDetails: 86400 }` | Cache TTL in seconds |
144
+
145
+ ### AutocompleteOptions
146
+
147
+ | Option | Type | Description |
148
+ |--------|------|-------------|
149
+ | `components` | `string` | Restrict results to specific countries (e.g., `'country:au'`) |
150
+ | `location` | `string` | Location bias (e.g., `'-37.8136,144.9631'`) |
151
+ | `radius` | `number` | Radius in meters for location bias |
152
+ | `types` | `string` | Restrict results to specific place types |
153
+ | `language` | `string` | Language code for results (e.g., `'en'`) |
154
+
155
+ ## ParsedAddress Type
156
+
157
+ The `onChange` callback receives a `ParsedAddress` object matching the `pace_address` table structure:
158
+
159
+ ```typescript
160
+ interface ParsedAddress {
161
+ place_id: string; // Always included - stable identifier
162
+ full_address: string | null;
163
+ street_number: string | null;
164
+ route: string | null;
165
+ suburb: string | null;
166
+ state: string | null;
167
+ postcode: string | null;
168
+ country: string | null;
169
+ lat: number | null;
170
+ lng: number | null;
171
+ }
172
+ ```
173
+
174
+ ## Caching Strategy
175
+
176
+ The component uses intelligent caching to reduce API costs:
177
+
178
+ - **Autocomplete Results**: Cached for 1 hour (default) by query string
179
+ - **Place Details**: Cached for 24 hours (default) by `place_id`
180
+ - **Request Deduplication**: Prevents duplicate simultaneous requests
181
+
182
+ ### Why This Matters
183
+
184
+ - **Cost Savings**: Repeated searches for the same address don't trigger new API calls
185
+ - **Performance**: Cached results are returned instantly
186
+ - **place_id Storage**: Store `place_id` in your database to retrieve addresses later without autocomplete searches
187
+
188
+ ### Cache Configuration
189
+
190
+ ```tsx
191
+ <AddressField
192
+ apiKey={apiKey}
193
+ cacheEnabled={true} // Enable/disable caching
194
+ cacheTTL={{
195
+ autocomplete: 3600, // 1 hour in seconds
196
+ placeDetails: 86400, // 24 hours in seconds
197
+ }}
198
+ />
199
+ ```
200
+
201
+ ## place_id Usage
202
+
203
+ The `place_id` is a stable identifier for a location that doesn't change. It's crucial for:
204
+
205
+ 1. **Storing in Database**: Save `place_id` in your `pace_address` table
206
+ 2. **Retrieving Later**: Use `getAddressByPlaceId()` to get full address without autocomplete
207
+ 3. **Verification**: Verify addresses haven't changed
208
+ 4. **Cost Efficiency**: Place details lookups are cheaper than autocomplete searches
209
+
210
+ ### Example: Storing and Retrieving
211
+
212
+ ```tsx
213
+ // When user selects an address
214
+ <AddressField
215
+ apiKey={apiKey}
216
+ onChange={async (address) => {
217
+ // Store in database
218
+ await supabase.from('pace_address').insert({
219
+ place_id: address.place_id, // Store this!
220
+ full_address: address.full_address,
221
+ lat: address.lat,
222
+ lng: address.lng,
223
+ // ... other fields
224
+ });
225
+ }}
226
+ />
227
+
228
+ // Later, retrieve from stored place_id
229
+ const storedPlaceId = 'ChIJ123...';
230
+ const address = await getAddressByPlaceId(storedPlaceId, apiKey);
231
+ ```
232
+
233
+ ## Keyboard Navigation
234
+
235
+ - **Arrow Down**: Navigate to next suggestion
236
+ - **Arrow Up**: Navigate to previous suggestion
237
+ - **Enter**: Select highlighted suggestion
238
+ - **Escape**: Close suggestions dropdown
239
+ - **Tab**: Close suggestions and move to next field
240
+
241
+ ## Accessibility
242
+
243
+ The component follows WCAG 2.1 guidelines:
244
+
245
+ - **ARIA Attributes**: Proper `role`, `aria-expanded`, `aria-autocomplete`, `aria-controls`
246
+ - **Keyboard Navigation**: Full keyboard support
247
+ - **Screen Readers**: Proper announcements and labels
248
+ - **Focus Management**: Proper focus handling
249
+
250
+ ## Error Handling
251
+
252
+ The component handles various error scenarios:
253
+
254
+ - **Network Errors**: Displays user-friendly error message
255
+ - **API Key Errors**: Clear error about API key configuration
256
+ - **Rate Limiting**: Informs user about quota limits
257
+ - **Invalid Requests**: Validates input and displays errors
258
+
259
+ ## Integration with pace_address Table
260
+
261
+ The `ParsedAddress` type matches the `pace_address` table structure, making it easy to store results:
262
+
263
+ ```tsx
264
+ <AddressField
265
+ apiKey={apiKey}
266
+ onChange={async (address) => {
267
+ const { data, error } = await supabase
268
+ .from('pace_address')
269
+ .insert({
270
+ place_id: address.place_id,
271
+ full_address: address.full_address,
272
+ street_number: address.street_number,
273
+ route: address.route,
274
+ suburb: address.suburb,
275
+ state: address.state,
276
+ postcode: address.postcode,
277
+ country: address.country,
278
+ lat: address.lat,
279
+ lng: address.lng,
280
+ organisation_id: currentOrganisationId,
281
+ });
282
+ }}
283
+ />
284
+ ```
285
+
286
+ ## API Key Setup
287
+
288
+ 1. Get a Google Places API key from [Google Cloud Console](https://console.cloud.google.com/)
289
+ 2. Enable the following APIs:
290
+ - Places API
291
+ - Places API (New)
292
+ 3. Set up API key restrictions (recommended)
293
+ 4. Add to your environment variables:
294
+
295
+ ```env
296
+ VITE_GOOGLE_PLACES_API_KEY=your-api-key-here
297
+ ```
298
+
299
+ ## Best Practices
300
+
301
+ 1. **Store place_id**: Always save `place_id` in your database for later retrieval
302
+ 2. **Enable Caching**: Keep caching enabled to reduce API costs
303
+ 3. **Handle Errors**: Implement proper error handling in your `onChange` callback
304
+ 4. **Country Restrictions**: Use `autocompleteOptions.components` to restrict to specific countries
305
+ 5. **Debounce Delay**: Adjust `debounceDelay` based on your needs (default 300ms is usually good)
306
+
307
+ ## Troubleshooting
308
+
309
+ ### No suggestions appearing
310
+
311
+ - Check API key is valid and has Places API enabled
312
+ - Verify API key has billing enabled
313
+ - Check browser console for error messages
314
+ - Ensure input has at least 2-3 characters
315
+
316
+ ### Suggestions not closing
317
+
318
+ - Check if there are click handlers preventing blur events
319
+ - Verify `onBlur` is not being prevented
320
+
321
+ ### Caching not working
322
+
323
+ - Ensure `cacheEnabled` is `true` (default)
324
+ - Check that `useQueryCache` hook is working properly
325
+
326
+ ## Related Components
327
+
328
+ - `Input` - Base input component used by AddressField
329
+ - `Select` - Similar dropdown pattern for reference
330
+
331
+ ## Related Utilities
332
+
333
+ - `getAddressByPlaceId` - Retrieve address from stored `place_id`
334
+ - `fetchPlaceAutocomplete` - Direct API access (if needed)
335
+ - `fetchPlaceDetails` - Direct API access (if needed)
336
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file AddressField Component Exports
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/AddressField
5
+ * @since 0.1.0
6
+ */
7
+
8
+ export { AddressField } from './AddressField';
9
+ export type { AddressFieldProps, AddressFieldRef, ParsedAddress, AutocompleteOptions } from './types';
10
+