@salesforce/ui-bundle-template-app-react-sample-b2x 12.6.3 → 12.7.0

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 (43) hide show
  1. package/dist/CHANGELOG.md +9 -0
  2. package/dist/force-app/main/default/uiBundles/propertyrentalapp/package.json +6 -6
  3. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/graphql-operations-types.ts +1 -2
  4. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/appLayout.tsx +5 -4
  5. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/layout/VerticalNav.tsx +60 -27
  6. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestList.tsx +18 -17
  7. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertyListingCard.tsx +2 -2
  8. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertyMap.tsx +10 -2
  9. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertySearchFilters.tsx +1 -1
  10. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/button.tsx +2 -2
  11. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/checkbox.tsx +1 -1
  12. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/input.tsx +1 -1
  13. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/select.tsx +1 -1
  14. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/context/TenantAccessContext.tsx +23 -6
  15. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/forms/auth-form.tsx +13 -2
  16. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/hooks/form.tsx +12 -3
  17. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layout/centered-page-layout.tsx +8 -40
  18. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/TenantRoute.tsx +28 -4
  19. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/privateRouteLayout.tsx +1 -1
  20. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/menu/AuthMenu.tsx +43 -5
  21. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ChangePassword.tsx +1 -1
  22. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ForgotPassword.tsx +8 -4
  23. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Login.tsx +1 -1
  24. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Register.tsx +1 -1
  25. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ResetPassword.tsx +2 -1
  26. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx +30 -7
  27. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/api/objectSearchService.ts +5 -10
  28. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/PaginationControls.tsx +38 -3
  29. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useReanchorReadingCursor.ts +54 -0
  30. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Application.tsx +30 -8
  31. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Contact.tsx +95 -48
  32. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Home.tsx +34 -23
  33. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Maintenance.tsx +5 -1
  34. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertyDetails.tsx +48 -16
  35. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx +15 -5
  36. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/styles/global.css +2 -2
  37. package/dist/force-app/main/default/uiBundles/propertyrentalapp/tsconfig.tsbuildinfo +1 -1
  38. package/dist/package-lock.json +2 -2
  39. package/dist/package.json +1 -1
  40. package/dist/scripts/network-senders.mjs +84 -0
  41. package/dist/scripts/org-setup-xml.mjs +11 -6
  42. package/dist/scripts/org-setup.mjs +13 -5
  43. package/package.json +1 -1
@@ -4,7 +4,6 @@ import { Button } from "@/components/ui/button";
4
4
  import { Input } from "@/components/ui/input";
5
5
  import { Label } from "@/components/ui/label";
6
6
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
- import { CenteredPageLayout } from "@/features/authentication/layout/centered-page-layout";
8
7
  import { Loader2 } from "lucide-react";
9
8
  import { createContactUsLead } from "@/api/leads/leadApi";
10
9
  import { useAuth } from "@/features/authentication/context/AuthContext";
@@ -12,32 +11,35 @@ import { fetchUserProfile } from "@/features/authentication/api/userProfileApi";
12
11
  import { useAsyncData } from "@/hooks/useAsyncData";
13
12
  import type { UserInfo } from "@/api/leads/leadApi";
14
13
 
15
- function SuccessCard() {
16
- const containerRef = useRef<HTMLDivElement>(null);
14
+ function SuccessCard({ onAnotherMessage }: { onAnotherMessage: () => void }) {
15
+ const headingRef = useRef<HTMLHeadingElement>(null);
17
16
  useEffect(() => {
18
- containerRef.current?.focus();
17
+ headingRef.current?.focus();
19
18
  }, []);
20
19
  return (
21
- <div ref={containerRef} tabIndex={-1} role="status" aria-live="polite" className="outline-none">
22
- <Card>
23
- <CardHeader>
24
- <CardTitle role="heading" aria-level={2} className="text-xl">
25
- Message sent
26
- </CardTitle>
27
- </CardHeader>
28
- <CardContent className="space-y-4">
29
- <p className="text-muted-foreground">
30
- Thank you for reaching out. We've received your message and will get back to you soon.
31
- </p>
32
- <Button asChild variant="outline">
33
- <Link to="/">Back to Home</Link>
34
- </Button>
35
- <Button asChild>
36
- <Link to="/contact">Send another message</Link>
37
- </Button>
38
- </CardContent>
39
- </Card>
40
- </div>
20
+ <Card>
21
+ <CardHeader>
22
+ <h2
23
+ ref={headingRef}
24
+ tabIndex={-1}
25
+ aria-describedby="contact-success-description"
26
+ className="text-xl font-semibold"
27
+ >
28
+ Message sent
29
+ </h2>
30
+ </CardHeader>
31
+ <CardContent className="space-y-4">
32
+ <p id="contact-success-description" className="text-muted-foreground">
33
+ Thank you for reaching out. We've received your message and will get back to you soon.
34
+ </p>
35
+ <Button asChild variant="outline">
36
+ <Link to="/">Back to Home</Link>
37
+ </Button>
38
+ <Button type="button" onClick={onAnotherMessage}>
39
+ Send another message
40
+ </Button>
41
+ </CardContent>
42
+ </Card>
41
43
  );
42
44
  }
43
45
 
@@ -48,6 +50,7 @@ interface ContactFormProps {
48
50
  submitting: boolean;
49
51
  submitError: string | null;
50
52
  loading?: boolean;
53
+ focusOnReady?: boolean;
51
54
  }
52
55
 
53
56
  function ContactForm({
@@ -57,7 +60,14 @@ function ContactForm({
57
60
  submitting,
58
61
  submitError,
59
62
  loading,
63
+ focusOnReady,
60
64
  }: ContactFormProps) {
65
+ const formRef = useRef<HTMLFormElement>(null);
66
+ useEffect(() => {
67
+ if (focusOnReady && !loading && document.activeElement === document.body) {
68
+ formRef.current?.querySelector<HTMLElement>("input:not(:disabled), textarea")?.focus();
69
+ }
70
+ }, [focusOnReady, loading]);
61
71
  const [firstName, setFirstName] = useState("");
62
72
  const [lastName, setLastName] = useState("");
63
73
  const [email, setEmail] = useState("");
@@ -84,6 +94,7 @@ function ContactForm({
84
94
  </CardHeader>
85
95
  <CardContent>
86
96
  <form
97
+ ref={formRef}
87
98
  onSubmit={onSubmit}
88
99
  className="space-y-4"
89
100
  aria-disabled={loading}
@@ -180,11 +191,17 @@ export default function Contact() {
180
191
  const [submitting, setSubmitting] = useState(false);
181
192
  const [submitError, setSubmitError] = useState<string | null>(null);
182
193
  const [submitted, setSubmitted] = useState(false);
194
+ const [profileAttempt, setProfileAttempt] = useState(0);
195
+ const [focusForm, setFocusForm] = useState(false);
183
196
 
184
- const { data: userInfo } = useAsyncData(() => {
197
+ const {
198
+ data: userInfo,
199
+ loading: profileLoading,
200
+ error: profileError,
201
+ } = useAsyncData(() => {
185
202
  if (!isAuthenticated || !user?.id) return Promise.resolve(null);
186
203
  return fetchUserProfile<UserInfo>(user.id);
187
- }, [isAuthenticated, user?.id]);
204
+ }, [isAuthenticated, user?.id, profileAttempt]);
188
205
 
189
206
  const handleSubmit = useCallback(
190
207
  async (e: SubmitEvent<HTMLFormElement>) => {
@@ -224,34 +241,64 @@ export default function Contact() {
224
241
  [isAuthenticated, userInfo],
225
242
  );
226
243
 
227
- const isLoading = loading || (isAuthenticated && !userInfo);
244
+ const isLoading = loading || (isAuthenticated && profileLoading);
245
+ const profileUnavailable = isAuthenticated && !isLoading && (profileError || !userInfo);
228
246
 
229
247
  if (submitted) {
230
248
  return (
231
- <CenteredPageLayout contentMaxWidth="md">
232
- <h1 className="mb-2 text-2xl font-semibold text-foreground">Contact Us</h1>
233
- <p className="mb-6 text-muted-foreground">
234
- Have a question or feedback? Send us a message and we'll respond as soon as we can.
235
- </p>
236
- <SuccessCard />
237
- </CenteredPageLayout>
249
+ <div className="flex min-h-svh w-full justify-center p-6 md:p-10 items-start">
250
+ <div className="w-full max-w-md">
251
+ <h1 className="mb-2 text-2xl font-semibold text-foreground">Contact Us</h1>
252
+ <p className="mb-6 text-muted-foreground">
253
+ Have a question or feedback? Send us a message and we'll respond as soon as we can.
254
+ </p>
255
+ <SuccessCard
256
+ onAnotherMessage={() => {
257
+ setFocusForm(true);
258
+ setSubmitted(false);
259
+ }}
260
+ />
261
+ </div>
262
+ </div>
238
263
  );
239
264
  }
240
265
 
241
266
  return (
242
- <CenteredPageLayout contentMaxWidth="md">
243
- <h1 className="mb-2 text-2xl font-semibold text-foreground">Contact Us</h1>
244
- <p className="mb-6 text-muted-foreground">
245
- Have a question or feedback? Send us a message and we'll respond as soon as we can.
246
- </p>
247
- <ContactForm
248
- isAuthenticated={isAuthenticated}
249
- userInfo={userInfo}
250
- onSubmit={handleSubmit}
251
- submitting={submitting}
252
- submitError={submitError}
253
- loading={isLoading}
254
- />
255
- </CenteredPageLayout>
267
+ <div className="flex min-h-svh w-full justify-center p-6 md:p-10 items-start">
268
+ <div className="w-full max-w-md">
269
+ <h1 className="mb-2 text-2xl font-semibold text-foreground">Contact Us</h1>
270
+ <p className="mb-6 text-muted-foreground">
271
+ Have a question or feedback? Send us a message and we'll respond as soon as we can.
272
+ </p>
273
+ {profileUnavailable ? (
274
+ <Card>
275
+ <CardContent className="space-y-4 pt-6">
276
+ <p role="alert" className="text-destructive">
277
+ Unable to load your contact information. Please try again.
278
+ </p>
279
+ <Button
280
+ type="button"
281
+ onClick={() => {
282
+ setFocusForm(true);
283
+ setProfileAttempt((attempt) => attempt + 1);
284
+ }}
285
+ >
286
+ Try again
287
+ </Button>
288
+ </CardContent>
289
+ </Card>
290
+ ) : (
291
+ <ContactForm
292
+ isAuthenticated={isAuthenticated}
293
+ userInfo={userInfo}
294
+ onSubmit={handleSubmit}
295
+ submitting={submitting}
296
+ submitError={submitError}
297
+ loading={isLoading}
298
+ focusOnReady={focusForm}
299
+ />
300
+ )}
301
+ </div>
302
+ </div>
256
303
  );
257
304
  }
@@ -106,10 +106,11 @@ export default function Home() {
106
106
  text: string;
107
107
  } | null>(null);
108
108
 
109
- const { data: searchResult, loading: featuredLoading } = useAsyncData<PropertySearchResult>(
110
- () => searchProperties({ first: FEATURED_PAGE_SIZE }),
111
- [],
112
- );
109
+ const {
110
+ data: searchResult,
111
+ loading: featuredLoading,
112
+ error: featuredError,
113
+ } = useAsyncData<PropertySearchResult>(() => searchProperties({ first: FEATURED_PAGE_SIZE }), []);
113
114
 
114
115
  const featuredResults = useMemo(
115
116
  () =>
@@ -176,16 +177,20 @@ export default function Home() {
176
177
  <div className="space-y-0">
177
178
  {/* ——— Hero ——— */}
178
179
  <div className="relative w-full overflow-hidden rounded-2xl">
179
- <div className="relative aspect-[21/9] min-h-[280px] w-full md:aspect-[3/1] md:min-h-[320px]">
180
+ <div className="relative grid min-h-[280px] w-full md:min-h-[320px]">
181
+ <div
182
+ aria-hidden="true"
183
+ className="col-start-1 row-start-1 aspect-[21/9] w-full self-start md:aspect-[3/1]"
184
+ />
180
185
  <img
181
186
  src={HERO_IMAGE}
182
187
  alt=""
183
- className="h-full w-full object-cover"
188
+ className="absolute inset-0 h-full w-full object-cover"
184
189
  loading="eager"
185
190
  fetchPriority="high"
186
191
  />
187
- <div className="absolute inset-0 bg-black/40" />
188
- <div className="absolute inset-0 flex flex-col items-center justify-center gap-6 px-4">
192
+ <div className="absolute inset-0 bg-black/65" />
193
+ <div className="relative col-start-1 row-start-1 flex min-w-0 flex-col items-center justify-center gap-6 px-4 py-6">
189
194
  <div className="w-full max-w-2xl text-center">
190
195
  <p className="mb-2 text-xs font-medium uppercase tracking-[0.2em] text-white/80">
191
196
  The Zen Way to Lease
@@ -212,7 +217,7 @@ export default function Home() {
212
217
  <Button
213
218
  type="submit"
214
219
  size="lg"
215
- className="min-h-12 shrink-0 cursor-pointer rounded-xl bg-primary px-8 font-medium text-primary-foreground shadow-md transition-colors duration-200 hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2"
220
+ className="h-auto min-h-12 shrink-0 cursor-pointer whitespace-normal rounded-xl bg-primary px-8 font-medium text-primary-foreground shadow-md transition-colors duration-200 hover:bg-primary/95 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2"
216
221
  >
217
222
  Find Home
218
223
  </Button>
@@ -237,12 +242,17 @@ export default function Home() {
237
242
  Featured Properties
238
243
  </h2>
239
244
  {featuredLoading ? (
240
- <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
241
- {[1, 2, 3].map((i) => (
242
- <PropertyListingCardSkeleton key={i} />
243
- ))}
244
- </div>
245
- ) : validFeatured.length > 0 ? (
245
+ <>
246
+ <p role="status" className="sr-only">
247
+ Loading featured properties…
248
+ </p>
249
+ <div aria-hidden="true" className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
250
+ {[1, 2, 3].map((i) => (
251
+ <PropertyListingCardSkeleton key={i} />
252
+ ))}
253
+ </div>
254
+ </>
255
+ ) : !featuredError && validFeatured.length > 0 ? (
246
256
  <FeaturedPropertiesGrid
247
257
  results={validFeatured}
248
258
  primaryImagesMap={primaryImagesMap}
@@ -250,11 +260,12 @@ export default function Home() {
250
260
  amenitiesMap={amenitiesMap}
251
261
  />
252
262
  ) : (
253
- <div
254
- role="status"
255
- className="rounded-xl bg-white/60 px-6 py-10 text-center text-muted-foreground"
256
- >
257
- <p>No featured properties at the moment.</p>
263
+ <div className="rounded-xl bg-white/60 px-6 py-10 text-center text-muted-foreground">
264
+ <p role={featuredError ? "alert" : "status"}>
265
+ {featuredError
266
+ ? "Unable to load featured properties. Please try again later."
267
+ : "No featured properties at the moment."}
268
+ </p>
258
269
  <Button asChild className="mt-3">
259
270
  <Link to="/properties">Browse all properties</Link>
260
271
  </Button>
@@ -287,7 +298,7 @@ export default function Home() {
287
298
  <Button
288
299
  asChild
289
300
  size="lg"
290
- className="cursor-pointer rounded-xl bg-primary transition-colors duration-200 hover:bg-primary/90"
301
+ className="cursor-pointer rounded-xl bg-primary transition-colors duration-200 [a]:hover:bg-primary/95"
291
302
  >
292
303
  <Link to="/contact">More About Us</Link>
293
304
  </Button>
@@ -471,7 +482,7 @@ export default function Home() {
471
482
  <h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-white">
472
483
  Explore
473
484
  </h3>
474
- <ul className="space-y-2 text-sm">
485
+ <ul role="list" className="space-y-2 text-sm">
475
486
  <li>
476
487
  <Link to="/" className="text-teal-200 hover:text-white">
477
488
  Home
@@ -493,7 +504,7 @@ export default function Home() {
493
504
  <h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-white">
494
505
  Support
495
506
  </h3>
496
- <ul className="space-y-2 text-sm">
507
+ <ul role="list" className="space-y-2 text-sm">
497
508
  <li>
498
509
  <Link to="/contact" className="text-teal-200 hover:text-white">
499
510
  Contact Us
@@ -19,6 +19,7 @@ import {
19
19
  type PaginationConfig,
20
20
  } from "@/features/object-search/hooks/useObjectSearchParams";
21
21
  import { useAsyncData } from "@/hooks/useAsyncData";
22
+ import { useReanchorReadingCursor } from "@/hooks/useReanchorReadingCursor";
22
23
  import PaginationControls from "@/features/object-search/components/PaginationControls";
23
24
  import type {
24
25
  Maintenance_Request__C_Filter,
@@ -113,6 +114,8 @@ export default function Maintenance() {
113
114
  [query.where, query.orderBy, pagination.pageSize, pagination.afterCursor, refreshCounter],
114
115
  );
115
116
 
117
+ useReanchorReadingCursor(loading);
118
+
116
119
  const requests = useMemo(
117
120
  () =>
118
121
  (searchResult?.edges ?? []).reduce<MaintenanceRequestNode[]>((acc, edge) => {
@@ -340,7 +343,8 @@ export default function Maintenance() {
340
343
  emptyMessage="No maintenance requests yet. Submit one above."
341
344
  />
342
345
  </CardContent>
343
- {!loading && !error && requests.length > 0 && (
346
+ {!error && requests.length > 0 && (
347
+ // Keep pagination mounted while results reload.
344
348
  <div className="px-6 pb-6">
345
349
  <PaginationControls
346
350
  pageIndex={pagination.pageIndex}
@@ -1,3 +1,4 @@
1
+ import { useEffect, useRef } from "react";
1
2
  import { useParams, Link } from "react-router";
2
3
  import { Button } from "@/components/ui/button";
3
4
  import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
@@ -38,7 +39,7 @@ function formatDate(val: string | null): string {
38
39
 
39
40
  function PropertyDetailsSkeleton() {
40
41
  return (
41
- <div className="mx-auto max-w-[900px]" role="status">
42
+ <div className="mx-auto max-w-[900px]">
42
43
  <h1 className="sr-only">Property details</h1>
43
44
  <Skeleton className="mb-4 h-4 w-32" />
44
45
 
@@ -100,8 +101,6 @@ function PropertyDetailsSkeleton() {
100
101
  </Card>
101
102
 
102
103
  <Skeleton className="mb-4 h-[52px] w-full rounded-xl" />
103
-
104
- <span className="sr-only">Loading property details…</span>
105
104
  </div>
106
105
  );
107
106
  }
@@ -110,6 +109,31 @@ export default function PropertyDetails() {
110
109
  const { id } = useParams<{ id: string }>();
111
110
  const { property, loading, error } = usePropertyDetail(id);
112
111
 
112
+ const containerRef = useRef<HTMLDivElement>(null);
113
+ const statusRef = useRef<HTMLParagraphElement>(null);
114
+ useEffect(() => {
115
+ let message = loading ? "Loading property details…" : "";
116
+ const heading = containerRef.current?.querySelector<HTMLElement>("h1");
117
+ if (!loading && heading) {
118
+ const active = document.activeElement;
119
+ if (
120
+ active &&
121
+ active !== document.body &&
122
+ active !== heading &&
123
+ active !== containerRef.current?.closest("main")
124
+ ) {
125
+ message = heading.textContent ?? "";
126
+ } else {
127
+ if (!heading.hasAttribute("tabindex")) heading.setAttribute("tabindex", "-1");
128
+ heading.focus();
129
+ }
130
+ }
131
+ if (statusRef.current && statusRef.current.textContent !== message) {
132
+ statusRef.current.textContent = message;
133
+ }
134
+ }, [loading, error, property, id]);
135
+ const status = <p ref={statusRef} role="status" className="sr-only" />;
136
+
113
137
  // Extract nested relationships from the single property node.
114
138
  const images = (property?.Property_Images__r?.edges ?? []).flatMap((e) =>
115
139
  e?.node ? [e.node] : [],
@@ -136,13 +160,21 @@ export default function PropertyDetails() {
136
160
  : geocodedCoords;
137
161
 
138
162
  if (loading) {
139
- return <PropertyDetailsSkeleton />;
163
+ return (
164
+ <div ref={containerRef}>
165
+ {status}
166
+ <PropertyDetailsSkeleton />
167
+ </div>
168
+ );
140
169
  }
141
170
 
142
171
  if (error) {
143
172
  return (
144
- <div className="mx-auto max-w-[900px]">
145
- <h1 className="sr-only">Property details</h1>
173
+ <div ref={containerRef} className="mx-auto max-w-[900px]">
174
+ {status}
175
+ <h1 className="sr-only" aria-describedby="property-details-error">
176
+ Unable to load property details
177
+ </h1>
146
178
  <div className="mb-4">
147
179
  <Link to="/properties" className="text-sm text-primary no-underline hover:underline">
148
180
  ← Back to listings
@@ -150,7 +182,7 @@ export default function PropertyDetails() {
150
182
  </div>
151
183
  <Card className="rounded-2xl border border-border shadow-sm">
152
184
  <CardContent className="pt-6">
153
- <p className="text-destructive" role="alert">
185
+ <p className="text-destructive" id="property-details-error">
154
186
  Something went wrong. Please try again later.
155
187
  </p>
156
188
  </CardContent>
@@ -161,8 +193,9 @@ export default function PropertyDetails() {
161
193
 
162
194
  if (!property && id) {
163
195
  return (
164
- <div className="mx-auto max-w-[900px]">
165
- <h1 className="sr-only">Property details</h1>
196
+ <div ref={containerRef} className="mx-auto max-w-[900px]">
197
+ {status}
198
+ <h1 className="sr-only">Listing not found</h1>
166
199
  <div className="mb-4">
167
200
  <Link to="/properties" className="text-sm text-primary no-underline hover:underline">
168
201
  ← Back to listings
@@ -170,9 +203,7 @@ export default function PropertyDetails() {
170
203
  </div>
171
204
  <Card className="rounded-2xl border border-border shadow-sm">
172
205
  <CardContent className="pt-6">
173
- <p className="text-destructive" role="status">
174
- Listing not found.
175
- </p>
206
+ <p className="text-destructive">Listing not found.</p>
176
207
  </CardContent>
177
208
  </Card>
178
209
  </div>
@@ -183,7 +214,8 @@ export default function PropertyDetails() {
183
214
  const otherImages = images.filter((i) => i.Id !== primaryImage?.Id);
184
215
 
185
216
  return (
186
- <div className="mx-auto max-w-[900px]">
217
+ <div ref={containerRef} className="mx-auto max-w-[900px]">
218
+ {status}
187
219
  <div className="mb-4">
188
220
  <Link to="/properties" className="text-sm text-primary no-underline hover:underline">
189
221
  ← Back to listings
@@ -299,7 +331,7 @@ export default function PropertyDetails() {
299
331
  </CardTitle>
300
332
  </CardHeader>
301
333
  <CardContent>
302
- <ul className="space-y-2">
334
+ <ul role="list" className="space-y-2">
303
335
  {costs.slice(0, 10).map((c) => (
304
336
  <li
305
337
  key={c.Id}
@@ -340,7 +372,7 @@ export default function PropertyDetails() {
340
372
  {features.map((f) => (
341
373
  <span
342
374
  key={f.Id}
343
- className="rounded-full border border-border bg-muted/60 px-2.5 py-0.5 text-xs font-medium text-muted-foreground"
375
+ className="rounded-full border border-border bg-muted/60 px-2.5 py-0.5 text-xs font-medium text-neutral-600 dark:text-neutral-300"
344
376
  >
345
377
  {f.Feature_Category__c?.value ? `${f.Feature_Category__c.value}: ` : ""}
346
378
  {f.Description__c?.value ?? f.Name?.value ?? "—"}
@@ -355,7 +387,7 @@ export default function PropertyDetails() {
355
387
  <Button
356
388
  asChild
357
389
  size="sm"
358
- className="w-full cursor-pointer rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 hover:bg-primary/90"
390
+ className="w-full cursor-pointer rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 [a]:hover:bg-primary/95"
359
391
  >
360
392
  <Link to={`/application?propertyId=${encodeURIComponent(id ?? "")}`}>
361
393
  Fill out an application
@@ -19,6 +19,7 @@ import {
19
19
  type PaginationConfig,
20
20
  } from "@/features/object-search/hooks/useObjectSearchParams";
21
21
  import { useAsyncData } from "@/hooks/useAsyncData";
22
+ import { useReanchorReadingCursor } from "@/hooks/useReanchorReadingCursor";
22
23
  import type { FilterFieldConfig } from "@/features/object-search/utils/filterUtils";
23
24
  import type { SortFieldConfig, SortState } from "@/features/object-search/utils/sortUtils";
24
25
  import { usePropertyMapMarkers } from "@/hooks/usePropertyMapMarkers";
@@ -119,6 +120,8 @@ export default function PropertySearch() {
119
120
  [query.where, query.orderBy, pagination.pageSize, pagination.afterCursor],
120
121
  );
121
122
 
123
+ useReanchorReadingCursor(resultsLoading);
124
+
122
125
  /* ── Derive results + maps ── */
123
126
  const results = useMemo(
124
127
  () =>
@@ -181,23 +184,24 @@ export default function PropertySearch() {
181
184
  /* ── Staged filter state for popover edits (committed on Save) ── */
182
185
  const committedSearchValue = filters.active.find((f) => f.field === "search")?.value ?? "";
183
186
  const [searchQuery, setSearchQuery] = useState(committedSearchValue);
187
+ const { set: setFilter, remove: removeFilter } = filters;
184
188
 
185
189
  // Debounce search text → filters.set
186
190
  useEffect(() => {
187
191
  const t = setTimeout(() => {
188
192
  if (searchQuery.trim()) {
189
- filters.set("search", {
193
+ setFilter("search", {
190
194
  field: "search",
191
195
  label: "Search",
192
196
  type: "search",
193
197
  value: searchQuery,
194
198
  });
195
199
  } else {
196
- filters.remove("search");
200
+ removeFilter("search");
197
201
  }
198
202
  }, SEARCH_FILTER_DEBOUNCE_MS);
199
203
  return () => clearTimeout(t);
200
- }, [searchQuery, filters]);
204
+ }, [searchQuery, setFilter, removeFilter]);
201
205
 
202
206
  // Sync local searchQuery when the committed value changes externally (e.g. URL nav)
203
207
  const [prevCommittedSearchValue, setPrevCommittedSearchValue] = useState(committedSearchValue);
@@ -413,7 +417,8 @@ export default function PropertySearch() {
413
417
  >
414
418
  {apiUnavailable ? (
415
419
  <PropertySearchPlaceholder message="Search is temporarily unavailable." />
416
- ) : resultsLoading ? (
420
+ ) : resultsLoading && results.length === 0 ? (
421
+ // Keep loaded results mounted to preserve focus during reloads.
417
422
  <div className="space-y-4">
418
423
  {[1, 2, 3].map((i) => (
419
424
  <PropertyListingCardSkeleton key={i} />
@@ -440,7 +445,12 @@ export default function PropertySearch() {
440
445
  </div>
441
446
  ) : (
442
447
  <>
443
- <ul className="space-y-4" role="list" aria-label="Search results">
448
+ <ul
449
+ className="space-y-4"
450
+ role="list"
451
+ aria-label="Search results"
452
+ aria-busy={resultsLoading || undefined}
453
+ >
444
454
  {visibleResults.map((node, index) => {
445
455
  const imageUrl = primaryImagesMap[node.Id] ?? null;
446
456
  const address = propertyAddressMap[node.Id] ?? null;
@@ -75,7 +75,7 @@
75
75
  --destructive: oklch(0.577 0.245 27.325);
76
76
  --border: oklch(0.922 0 0);
77
77
  --input: oklch(0.922 0 0);
78
- --ring: oklch(0.708 0 0);
78
+ --ring: oklch(0.556 0 0);
79
79
  --chart-1: oklch(0.646 0.222 41.116);
80
80
  --chart-2: oklch(0.6 0.118 184.704);
81
81
  --chart-3: oklch(0.398 0.07 227.392);
@@ -127,7 +127,7 @@
127
127
 
128
128
  @layer base {
129
129
  * {
130
- @apply border-border outline-ring/50;
130
+ @apply border-border outline-ring;
131
131
  }
132
132
  body {
133
133
  @apply bg-background text-foreground;
@@ -1 +1 @@
1
- {"root":["./src/app.tsx","./src/appLayout.tsx","./src/navigationMenu.tsx","./src/router-utils.tsx","./src/routes.tsx","./src/api/graphql-operations-types.ts","./src/api/graphqlClient.ts","./src/api/applications/applicationApi.ts","./src/api/leads/leadApi.ts","./src/api/maintenanceRequests/maintenanceRequestApi.ts","./src/api/properties/propertyDetailGraphQL.ts","./src/api/properties/propertyNodeUtils.ts","./src/api/properties/propertySearchService.ts","./src/api/tenants/tenantApi.ts","./src/components/ErrorBoundary.tsx","./src/components/SkeletonPrimitives.tsx","./src/components/alerts/status-alert.tsx","./src/components/dashboard/WeatherWidget.tsx","./src/components/layout/TopBar.tsx","./src/components/layout/VerticalNav.tsx","./src/components/layouts/card-layout.tsx","./src/components/maintenanceRequests/MaintenanceRequestIcon.tsx","./src/components/maintenanceRequests/MaintenanceRequestList.tsx","./src/components/maintenanceRequests/MaintenanceRequestListItem.tsx","./src/components/maintenanceRequests/MaintenanceSummaryDetailsModal.tsx","./src/components/maintenanceRequests/StatusBadge.tsx","./src/components/properties/PropertyListingCard.tsx","./src/components/properties/PropertyMap.tsx","./src/components/properties/PropertySearchFilters.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/datePicker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/field.tsx","./src/components/ui/index.ts","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/spinner.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/context/TenantAccessContext.tsx","./src/features/authentication/authHelpers.ts","./src/features/authentication/authenticationConfig.ts","./src/features/authentication/api/userProfileApi.ts","./src/features/authentication/components/SocialLogin.tsx","./src/features/authentication/context/AuthContext.tsx","./src/features/authentication/footers/footer-link.tsx","./src/features/authentication/forms/auth-form.tsx","./src/features/authentication/forms/submit-button.tsx","./src/features/authentication/hooks/form.tsx","./src/features/authentication/hooks/useCountdownTimer.ts","./src/features/authentication/hooks/useRetryWithBackoff.ts","./src/features/authentication/layout/centered-page-layout.tsx","./src/features/authentication/layouts/AuthAppLayout.tsx","./src/features/authentication/layouts/TenantRoute.tsx","./src/features/authentication/layouts/privateRouteLayout.tsx","./src/features/authentication/menu/AuthMenu.tsx","./src/features/authentication/pages/ChangePassword.tsx","./src/features/authentication/pages/ForgotPassword.tsx","./src/features/authentication/pages/Login.tsx","./src/features/authentication/pages/Profile.tsx","./src/features/authentication/pages/Register.tsx","./src/features/authentication/pages/ResetPassword.tsx","./src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx","./src/features/authentication/sessionTimeout/sessionTimeService.ts","./src/features/authentication/sessionTimeout/sessionTimeoutConfig.ts","./src/features/authentication/utils/helpers.ts","./src/features/object-search/api/objectSearchService.ts","./src/features/object-search/components/PaginationControls.tsx","./src/features/object-search/hooks/useObjectSearchParams.ts","./src/features/object-search/utils/debounce.ts","./src/features/object-search/utils/filterUtils.ts","./src/features/object-search/utils/sortUtils.ts","./src/hooks/useAsyncData.ts","./src/hooks/useGeocode.ts","./src/hooks/useMaintenanceRequests.ts","./src/hooks/usePropertyDetail.ts","./src/hooks/usePropertyMapMarkers.ts","./src/hooks/useWeather.ts","./src/lib/utils.ts","./src/pages/Application.tsx","./src/pages/Contact.tsx","./src/pages/Dashboard.tsx","./src/pages/Home.tsx","./src/pages/Maintenance.tsx","./src/pages/NotFound.tsx","./src/pages/PropertyDetails.tsx","./src/pages/PropertySearch.tsx","./src/pages/PropertySearchPlaceholder.tsx","./src/types/leaflet.d.ts","./src/types/searchResults.ts","./src/utils/geocode.ts","./src/utils/propertyListingPaginationUtils.ts","./e2e/app.spec.ts","./vite-env.d.ts","./vitest-env.d.ts","./vitest.setup.ts","../../../../../../../../../sdk/platform-sdk/types/env.d.ts"],"version":"5.9.3"}
1
+ {"root":["./src/app.tsx","./src/appLayout.tsx","./src/navigationMenu.tsx","./src/router-utils.tsx","./src/routes.tsx","./src/api/graphql-operations-types.ts","./src/api/graphqlClient.ts","./src/api/applications/applicationApi.ts","./src/api/leads/leadApi.ts","./src/api/maintenanceRequests/maintenanceRequestApi.ts","./src/api/properties/propertyDetailGraphQL.ts","./src/api/properties/propertyNodeUtils.ts","./src/api/properties/propertySearchService.ts","./src/api/tenants/tenantApi.ts","./src/components/ErrorBoundary.tsx","./src/components/SkeletonPrimitives.tsx","./src/components/alerts/status-alert.tsx","./src/components/dashboard/WeatherWidget.tsx","./src/components/layout/TopBar.tsx","./src/components/layout/VerticalNav.tsx","./src/components/layouts/card-layout.tsx","./src/components/maintenanceRequests/MaintenanceRequestIcon.tsx","./src/components/maintenanceRequests/MaintenanceRequestList.tsx","./src/components/maintenanceRequests/MaintenanceRequestListItem.tsx","./src/components/maintenanceRequests/MaintenanceSummaryDetailsModal.tsx","./src/components/maintenanceRequests/StatusBadge.tsx","./src/components/properties/PropertyListingCard.tsx","./src/components/properties/PropertyMap.tsx","./src/components/properties/PropertySearchFilters.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/datePicker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/field.tsx","./src/components/ui/index.ts","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/spinner.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/context/TenantAccessContext.tsx","./src/features/authentication/authHelpers.ts","./src/features/authentication/authenticationConfig.ts","./src/features/authentication/api/userProfileApi.ts","./src/features/authentication/components/SocialLogin.tsx","./src/features/authentication/context/AuthContext.tsx","./src/features/authentication/footers/footer-link.tsx","./src/features/authentication/forms/auth-form.tsx","./src/features/authentication/forms/submit-button.tsx","./src/features/authentication/hooks/form.tsx","./src/features/authentication/hooks/useCountdownTimer.ts","./src/features/authentication/hooks/useRetryWithBackoff.ts","./src/features/authentication/layout/centered-page-layout.tsx","./src/features/authentication/layouts/AuthAppLayout.tsx","./src/features/authentication/layouts/TenantRoute.tsx","./src/features/authentication/layouts/privateRouteLayout.tsx","./src/features/authentication/menu/AuthMenu.tsx","./src/features/authentication/pages/ChangePassword.tsx","./src/features/authentication/pages/ForgotPassword.tsx","./src/features/authentication/pages/Login.tsx","./src/features/authentication/pages/Profile.tsx","./src/features/authentication/pages/Register.tsx","./src/features/authentication/pages/ResetPassword.tsx","./src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx","./src/features/authentication/sessionTimeout/sessionTimeService.ts","./src/features/authentication/sessionTimeout/sessionTimeoutConfig.ts","./src/features/authentication/utils/helpers.ts","./src/features/object-search/api/objectSearchService.ts","./src/features/object-search/components/PaginationControls.tsx","./src/features/object-search/hooks/useObjectSearchParams.ts","./src/features/object-search/utils/debounce.ts","./src/features/object-search/utils/filterUtils.ts","./src/features/object-search/utils/sortUtils.ts","./src/hooks/useAsyncData.ts","./src/hooks/useGeocode.ts","./src/hooks/useMaintenanceRequests.ts","./src/hooks/usePropertyDetail.ts","./src/hooks/usePropertyMapMarkers.ts","./src/hooks/useReanchorReadingCursor.ts","./src/hooks/useWeather.ts","./src/lib/utils.ts","./src/pages/Application.tsx","./src/pages/Contact.tsx","./src/pages/Dashboard.tsx","./src/pages/Home.tsx","./src/pages/Maintenance.tsx","./src/pages/NotFound.tsx","./src/pages/PropertyDetails.tsx","./src/pages/PropertySearch.tsx","./src/pages/PropertySearchPlaceholder.tsx","./src/types/leaflet.d.ts","./src/types/searchResults.ts","./src/utils/geocode.ts","./src/utils/propertyListingPaginationUtils.ts","./e2e/app.spec.ts","./vite-env.d.ts","./vitest-env.d.ts","./vitest.setup.ts","../../../../../../../../../sdk/platform-sdk/types/env.d.ts"],"version":"5.9.3"}
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
3
- "version": "12.6.3",
3
+ "version": "12.7.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
9
- "version": "12.6.3",
9
+ "version": "12.7.0",
10
10
  "license": "SEE LICENSE IN LICENSE.txt",
11
11
  "dependencies": {
12
12
  "fast-xml-parser": "^5.9.3",
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
3
- "version": "12.6.3",
3
+ "version": "12.7.0",
4
4
  "description": "Base SFDX project template",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "publishConfig": {