@salesforce/ui-bundle-template-app-react-sample-b2x 11.56.2 → 11.56.4

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 (24) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/uiBundles/propertyrentalapp/package.json +4 -4
  3. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/app.tsx +2 -24
  4. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/appLayout.tsx +60 -5
  5. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ErrorBoundary.tsx +24 -0
  6. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/dashboard/WeatherWidget.tsx +10 -2
  7. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/layout/TopBar.tsx +13 -7
  8. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/layout/VerticalNav.tsx +12 -1
  9. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestList.tsx +9 -2
  10. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestListItem.tsx +18 -1
  11. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertyMap.tsx +1 -0
  12. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertySearchFilters.tsx +8 -8
  13. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useWeather.ts +1 -1
  14. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Application.tsx +25 -4
  15. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Contact.tsx +35 -21
  16. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Dashboard.tsx +1 -0
  17. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Home.tsx +11 -8
  18. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Maintenance.tsx +9 -5
  19. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertyDetails.tsx +19 -8
  20. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx +15 -4
  21. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/styles/global.css +1 -1
  22. package/dist/package-lock.json +2 -2
  23. package/dist/package.json +1 -1
  24. package/package.json +1 -1
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [11.56.4](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.56.3...v11.56.4) (2026-08-13)
7
+
8
+ **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
9
+
10
+
11
+
12
+
13
+
14
+ ## [11.56.3](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.56.2...v11.56.3) (2026-08-12)
15
+
16
+ **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
17
+
18
+
19
+
20
+
21
+
6
22
  ## [11.56.2](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.56.1...v11.56.2) (2026-08-12)
7
23
 
8
24
  **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
@@ -18,8 +18,8 @@
18
18
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
19
19
  },
20
20
  "dependencies": {
21
- "@salesforce/platform-sdk": "^11.56.2",
22
- "@salesforce/ui-bundle": "^11.56.2",
21
+ "@salesforce/platform-sdk": "^11.56.4",
22
+ "@salesforce/ui-bundle": "^11.56.4",
23
23
  "@tailwindcss/vite": "^4.1.17",
24
24
  "class-variance-authority": "^0.7.1",
25
25
  "clsx": "^2.1.1",
@@ -50,8 +50,8 @@
50
50
  "@graphql-eslint/eslint-plugin": "^4.1.0",
51
51
  "@graphql-tools/utils": "^11.0.0",
52
52
  "@playwright/test": "^1.49.0",
53
- "@salesforce/graphiti": "^11.56.2",
54
- "@salesforce/vite-plugin-ui-bundle": "^11.56.2",
53
+ "@salesforce/graphiti": "^11.56.4",
54
+ "@salesforce/vite-plugin-ui-bundle": "^11.56.4",
55
55
  "@testing-library/jest-dom": "^6.6.3",
56
56
  "@testing-library/react": "^16.1.0",
57
57
  "@testing-library/user-event": "^14.5.2",
@@ -1,32 +1,10 @@
1
1
  import { createBrowserRouter, RouterProvider } from "react-router";
2
2
  import { routes } from "@/routes";
3
- import { StrictMode, Component, type ReactNode } from "react";
3
+ import { StrictMode } from "react";
4
4
  import { createRoot } from "react-dom/client";
5
+ import { ErrorBoundary } from "@/components/ErrorBoundary";
5
6
  import "./styles/global.css";
6
7
 
7
- class ErrorBoundary extends Component<
8
- { children: ReactNode },
9
- { hasError: boolean; error: Error | null }
10
- > {
11
- state = { hasError: false, error: null as Error | null };
12
- static getDerivedStateFromError(error: Error) {
13
- return { hasError: true, error };
14
- }
15
- render() {
16
- if (this.state.hasError && this.state.error) {
17
- return (
18
- <div className="mx-auto max-w-[600px] p-8 font-sans">
19
- <h1 className="text-xl text-destructive">Something went wrong</h1>
20
- <pre className="mt-2 overflow-auto rounded-md bg-destructive/10 p-4 text-sm">
21
- {this.state.error.message}
22
- </pre>
23
- </div>
24
- );
25
- }
26
- return this.props.children;
27
- }
28
- }
29
-
30
8
  // Normalize basename: strip trailing slash so it matches URLs like /lwr/application/ai/c-app
31
9
  const rawBasePath = (globalThis as any).SFDC_ENV?.basePath;
32
10
  const basename = typeof rawBasePath === "string" ? rawBasePath.replace(/\/+$/, "") : undefined;
@@ -1,23 +1,78 @@
1
- import { useState } from "react";
2
- import { Outlet } from "react-router";
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { Outlet, useLocation } from "react-router";
3
3
  import { TopBar } from "@/components/layout/TopBar";
4
4
  import { VerticalNav } from "@/components/layout/VerticalNav";
5
5
  import { Toaster } from "@/components/ui/sonner";
6
6
  import { TenantAccessProvider } from "@/context/TenantAccessContext";
7
7
 
8
+ const APP_NAME = "ZENLEASE";
9
+
10
+ const atPath = (base: string) => (p: string) => p === base || p.startsWith(`${base}/`);
11
+
12
+ const PAGE_TITLES: { match: (path: string) => boolean; title: string }[] = [
13
+ { match: (p) => p === "/", title: "Home" },
14
+ { match: atPath("/properties"), title: "Property Search" },
15
+ {
16
+ match: (p) => p.startsWith("/property/") || p.startsWith("/object/"),
17
+ title: "Property Details",
18
+ },
19
+ { match: atPath("/dashboard"), title: "Dashboard" },
20
+ { match: atPath("/maintenance"), title: "Maintenance" },
21
+ { match: atPath("/contact"), title: "Contact" },
22
+ { match: atPath("/application"), title: "Application" },
23
+ ];
24
+
25
+ function pageTitleFor(pathname: string): string {
26
+ const page = PAGE_TITLES.find((entry) => entry.match(pathname))?.title ?? "Page Not Found";
27
+ return `${page} · ${APP_NAME}`;
28
+ }
29
+
8
30
  export default function AppLayout() {
9
31
  const [isNavOpen, setIsNavOpen] = useState(false);
32
+ const menuButtonRef = useRef<HTMLButtonElement>(null);
33
+ const mainRef = useRef<HTMLElement>(null);
34
+ const { pathname } = useLocation();
35
+ const previousPathname = useRef(pathname);
36
+
37
+ useEffect(() => {
38
+ document.title = pageTitleFor(pathname);
39
+ if (pathname !== previousPathname.current) {
40
+ previousPathname.current = pathname;
41
+ mainRef.current?.focus();
42
+ }
43
+ }, [pathname]);
44
+
45
+ const closeNav = () => {
46
+ setIsNavOpen(false);
47
+ menuButtonRef.current?.focus();
48
+ };
10
49
 
11
50
  return (
12
51
  <TenantAccessProvider>
13
52
  <div className="flex min-h-screen flex-col bg-gray-50">
14
53
  <Toaster />
15
- <TopBar onMenuClick={() => setIsNavOpen(true)} />
54
+ <a
55
+ href="#main-content"
56
+ className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded-lg focus:bg-primary focus:px-4 focus:py-2 focus:text-primary-foreground focus:ring-2 focus:ring-white focus:ring-offset-2"
57
+ >
58
+ Skip to main content
59
+ </a>
60
+ <TopBar
61
+ onMenuClick={() => setIsNavOpen((open) => !open)}
62
+ isNavOpen={isNavOpen}
63
+ menuButtonRef={menuButtonRef}
64
+ />
16
65
 
17
66
  <div className="flex flex-1 overflow-hidden">
18
- <VerticalNav isOpen={isNavOpen} onClose={() => setIsNavOpen(false)} />
67
+ <VerticalNav isOpen={isNavOpen} onClose={closeNav} />
19
68
 
20
- <main className="flex-1 overflow-auto bg-gray-50 p-4 md:p-8" role="main">
69
+ <main
70
+ id="main-content"
71
+ ref={mainRef}
72
+ tabIndex={-1}
73
+ className="flex-1 overflow-auto bg-gray-50 p-4 outline-none md:p-8"
74
+ role="main"
75
+ >
21
76
  <Outlet />
22
77
  </main>
23
78
  </div>
@@ -0,0 +1,24 @@
1
+ import { Component, type ReactNode } from "react";
2
+
3
+ export class ErrorBoundary extends Component<
4
+ { children: ReactNode },
5
+ { hasError: boolean; error: Error | null }
6
+ > {
7
+ state = { hasError: false, error: null as Error | null };
8
+ static getDerivedStateFromError(error: Error) {
9
+ return { hasError: true, error };
10
+ }
11
+ render() {
12
+ if (this.state.hasError && this.state.error) {
13
+ return (
14
+ <div role="alert" className="mx-auto max-w-[600px] p-8 font-sans">
15
+ <h1 className="text-xl text-destructive">Something went wrong</h1>
16
+ <pre className="mt-2 overflow-auto rounded-md bg-destructive/10 p-4 text-sm">
17
+ {this.state.error.message}
18
+ </pre>
19
+ </div>
20
+ );
21
+ }
22
+ return this.props.children;
23
+ }
24
+ }
@@ -1,7 +1,7 @@
1
1
  import { useRef, useState } from "react";
2
2
  import { Card } from "@/components/ui/card";
3
3
  import { Skeleton } from "@/components/ui/skeleton";
4
- import { useWeather, type WeatherData, type WeatherHour } from "@/hooks/useWeather";
4
+ import { useWeather, WEATHER_LABELS, type WeatherData, type WeatherHour } from "@/hooks/useWeather";
5
5
  import {
6
6
  Sun,
7
7
  Cloud,
@@ -256,6 +256,7 @@ function HourlyForecast({ hours, activeTab }: { hours: WeatherHour[]; activeTab:
256
256
  >
257
257
  <p className="whitespace-nowrap text-xs text-muted-foreground">{h.time}</p>
258
258
  {getWeatherIcon(h.weatherCode, "h-5 w-5 text-gray-500")}
259
+ <span className="sr-only">{WEATHER_LABELS[h.weatherCode] ?? "Unknown"}</span>
259
260
  <p className="text-base font-semibold text-foreground">{h.temp}°</p>
260
261
  </div>
261
262
  ))}
@@ -271,7 +272,14 @@ export function WeatherWidget() {
271
272
  <Card className="rounded-2xl border-0 p-6 shadow-md">
272
273
  <h2 className="text-lg font-semibold text-primary">Weather</h2>
273
274
 
274
- {loading && <WeatherSkeleton />}
275
+ {loading && (
276
+ <>
277
+ <span role="status" className="sr-only">
278
+ Loading weather
279
+ </span>
280
+ <WeatherSkeleton />
281
+ </>
282
+ )}
275
283
 
276
284
  {error && (
277
285
  <p className="mt-4 text-sm text-destructive" role="alert">
@@ -1,5 +1,6 @@
1
1
  import { ChevronDown, Menu, User } from "lucide-react";
2
2
  import { Link } from "react-router";
3
+ import type { RefObject } from "react";
3
4
  import { ROUTES } from "@/features/authentication/authenticationConfig";
4
5
  import zenLogo from "@/assets/icons/zen-logo.svg";
5
6
  import { Avatar, AvatarFallback } from "@/components/ui/avatar";
@@ -8,20 +9,25 @@ import { AuthMenu } from "@/features/authentication/menu/AuthMenu";
8
9
 
9
10
  export interface TopBarProps {
10
11
  onMenuClick?: () => void;
12
+ isNavOpen?: boolean;
13
+ menuButtonRef?: RefObject<HTMLButtonElement | null>;
11
14
  }
12
15
 
13
- export function TopBar({ onMenuClick }: TopBarProps) {
16
+ export function TopBar({ onMenuClick, isNavOpen, menuButtonRef }: TopBarProps) {
14
17
  return (
15
18
  <header className="flex h-16 items-center justify-between bg-teal-700 px-6 text-white">
16
19
  <div className="flex items-center gap-4">
17
20
  <Button
21
+ ref={menuButtonRef}
18
22
  variant="ghost"
19
23
  size="icon"
20
24
  onClick={onMenuClick}
21
- className="text-white hover:bg-teal-600 md:hidden"
25
+ className="text-white hover:bg-teal-600 aria-expanded:bg-teal-600 aria-expanded:text-white md:hidden"
22
26
  aria-label="Toggle menu"
27
+ aria-expanded={isNavOpen}
28
+ aria-controls={isNavOpen ? "mobile-nav" : undefined}
23
29
  >
24
- <Menu className="size-6" />
30
+ <Menu className="size-6" aria-hidden />
25
31
  </Button>
26
32
  <Logo />
27
33
  </div>
@@ -30,7 +36,7 @@ export function TopBar({ onMenuClick }: TopBarProps) {
30
36
  trigger={(user) => (
31
37
  <Button
32
38
  variant="ghost"
33
- className="h-10 gap-2 rounded-lg px-3 text-base text-white hover:bg-teal-600 hover:text-white"
39
+ className="h-10 gap-2 rounded-lg px-3 text-base text-white hover:bg-teal-800 hover:text-white"
34
40
  aria-label="User menu"
35
41
  >
36
42
  <Avatar className="size-7 bg-teal-300 text-teal-900">
@@ -39,7 +45,7 @@ export function TopBar({ onMenuClick }: TopBarProps) {
39
45
  </AvatarFallback>
40
46
  </Avatar>
41
47
  <span className="hidden font-medium md:inline">{user.name.toUpperCase()}</span>
42
- <ChevronDown className="hidden size-4 md:block" />
48
+ <ChevronDown className="hidden size-4 md:block" aria-hidden />
43
49
  </Button>
44
50
  )}
45
51
  guestContent={
@@ -49,8 +55,8 @@ export function TopBar({ onMenuClick }: TopBarProps) {
49
55
  size="icon"
50
56
  className="rounded-full bg-white text-teal-700 hover:bg-teal-50 md:h-9 md:w-auto md:gap-2 md:rounded-lg md:px-4"
51
57
  >
52
- <Link to={ROUTES.LOGIN.PATH}>
53
- <User className="size-5 md:size-4" />
58
+ <Link to={ROUTES.LOGIN.PATH} aria-label="Sign Up / Sign In">
59
+ <User className="size-5 md:size-4" aria-hidden="true" />
54
60
  <span className="hidden md:inline">Sign Up / Sign In</span>
55
61
  </Link>
56
62
  </Button>
@@ -2,7 +2,7 @@ import { Link, useLocation } from "react-router";
2
2
  import { Home, Search, BarChart3, Wrench, Phone, type LucideIcon } from "lucide-react";
3
3
  import { useAuth } from "@/features/authentication/context/AuthContext";
4
4
  import { useTenantAccess } from "@/context/TenantAccessContext";
5
- import { useMemo } from "react";
5
+ import { useEffect, useMemo } from "react";
6
6
 
7
7
  interface VerticalNavItem {
8
8
  path: string;
@@ -29,6 +29,15 @@ export function VerticalNav({ isOpen = false, onClose }: VerticalNavProps) {
29
29
  const { isAuthenticated } = useAuth();
30
30
  const { hasTenantRecord } = useTenantAccess();
31
31
 
32
+ useEffect(() => {
33
+ if (!isOpen) return;
34
+ const onKey = (e: KeyboardEvent) => {
35
+ if (e.key === "Escape") onClose?.();
36
+ };
37
+ document.addEventListener("keydown", onKey);
38
+ return () => document.removeEventListener("keydown", onKey);
39
+ }, [isOpen, onClose]);
40
+
32
41
  const visibleItems = useMemo(
33
42
  () =>
34
43
  navItems.filter((item) => {
@@ -63,6 +72,7 @@ export function VerticalNav({ isOpen = false, onClose }: VerticalNavProps) {
63
72
  }`}
64
73
  title={item.label}
65
74
  aria-label={item.label}
75
+ aria-current={active ? "page" : undefined}
66
76
  >
67
77
  <Icon className="size-6 shrink-0" aria-hidden />
68
78
  <span className="text-center text-xs font-medium leading-tight">{item.label}</span>
@@ -89,6 +99,7 @@ export function VerticalNav({ isOpen = false, onClose }: VerticalNavProps) {
89
99
  * same time, and two navigation landmarks sharing one name is ambiguous.
90
100
  */}
91
101
  <nav
102
+ id="mobile-nav"
92
103
  className="fixed left-0 top-0 bottom-0 z-50 flex w-24 flex-col border-r border-gray-200 bg-white py-8"
93
104
  aria-label="Mobile navigation"
94
105
  >
@@ -29,14 +29,21 @@ export default function MaintenanceRequestList({
29
29
  onClose={() => setSelectedRequest(null)}
30
30
  />
31
31
  )}
32
- {loading && <SkeletonListRows count={skeletonCount} />}
32
+ {loading && (
33
+ <div role="status" aria-live="polite" aria-atomic="true">
34
+ <SkeletonListRows count={skeletonCount} />
35
+ <span className="sr-only">Loading maintenance requests…</span>
36
+ </div>
37
+ )}
33
38
  {error && (
34
39
  <p className="py-4 text-sm text-destructive" role="alert">
35
40
  {error}
36
41
  </p>
37
42
  )}
38
43
  {!loading && !error && requests.length === 0 && (
39
- <div className="py-8 text-center text-gray-500">{emptyMessage}</div>
44
+ <div role="status" className="py-8 text-center text-gray-500">
45
+ {emptyMessage}
46
+ </div>
40
47
  )}
41
48
  {!loading &&
42
49
  !error &&
@@ -23,6 +23,16 @@ export default function MaintenanceRequestListItem({
23
23
  : (request.Name?.value ?? "—");
24
24
  const title = request.Description__c?.value?.trim() || request.Name?.value?.trim() || "—";
25
25
 
26
+ const rawStatus = request.Status__c?.value ?? null;
27
+ const statusLabel =
28
+ rawStatus === "New"
29
+ ? "Needs Action"
30
+ : rawStatus === "In Progress"
31
+ ? "In Progress"
32
+ : rawStatus === "Resolved"
33
+ ? "Resolved"
34
+ : rawStatus;
35
+
26
36
  const handleClick = useCallback(() => {
27
37
  onClick?.(request);
28
38
  }, [onClick, request]);
@@ -48,7 +58,14 @@ export default function MaintenanceRequestListItem({
48
58
  tabIndex={isClickable ? 0 : undefined}
49
59
  aria-label={
50
60
  isClickable
51
- ? `View details for ${title !== "—" ? title : "maintenance request"}`
61
+ ? [
62
+ `View details for ${title !== "—" ? title : "maintenance request"}`,
63
+ issueType,
64
+ addressFirstPart !== "—" ? addressFirstPart : null,
65
+ statusLabel ? `status ${statusLabel}` : null,
66
+ ]
67
+ .filter(Boolean)
68
+ .join(", ")
52
69
  : undefined
53
70
  }
54
71
  >
@@ -144,6 +144,7 @@ function MapContent({
144
144
  key={`${m.lat}-${m.lng}-${m.propertyId ?? i}`}
145
145
  position={[m.lat, m.lng]}
146
146
  icon={pinIcon}
147
+ title={m.label ?? "Property"}
147
148
  >
148
149
  <Popup>
149
150
  {popupContent ? (
@@ -5,13 +5,13 @@ import { Popover } from "radix-ui";
5
5
  import { Search, ChevronDown } from "lucide-react";
6
6
 
7
7
  const SEARCH_INPUT_CLASS =
8
- "h-10 rounded-full border-2 border-primary/50 bg-background px-4 text-sm outline-none transition-colors placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20";
8
+ "h-10 rounded-full border-2 border-primary bg-background px-4 text-sm outline-none transition-colors placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2";
9
9
 
10
10
  const FILTER_PILL_CLASS =
11
- "inline-flex h-10 items-center gap-1.5 rounded-full border-2 border-primary/50 bg-background px-4 text-sm font-medium text-foreground outline-none transition-colors hover:border-primary/70 focus:ring-2 focus:ring-primary/20";
11
+ "inline-flex h-10 items-center gap-1.5 rounded-full border-2 border-primary bg-background px-4 text-sm font-medium text-foreground outline-none transition-colors hover:bg-primary/10 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2";
12
12
 
13
13
  const SAVE_BUTTON_CLASS =
14
- "mt-3 w-full rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary/20";
14
+ "mt-3 w-full rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2";
15
15
 
16
16
  const DEFAULT_PRICE_MIN = 0;
17
17
  const DEFAULT_PRICE_MAX = 100_000;
@@ -161,7 +161,7 @@ export default function PropertySearchFilters({
161
161
  Price range
162
162
  </p>
163
163
  <div className="flex items-center gap-2">
164
- <div className="flex flex-1 items-center rounded-full border-2 border-primary/50 bg-background focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20">
164
+ <div className="flex flex-1 items-center rounded-full border-2 border-primary bg-background focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2">
165
165
  <span className="pl-4 text-sm text-muted-foreground">$</span>
166
166
  <input
167
167
  type="number"
@@ -176,7 +176,7 @@ export default function PropertySearchFilters({
176
176
  />
177
177
  </div>
178
178
  <span className="text-muted-foreground">–</span>
179
- <div className="flex flex-1 items-center rounded-full border-2 border-primary/50 bg-background focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20">
179
+ <div className="flex flex-1 items-center rounded-full border-2 border-primary bg-background focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2">
180
180
  <span className="pl-4 text-sm text-muted-foreground">$</span>
181
181
  <input
182
182
  type="number"
@@ -230,7 +230,7 @@ export default function PropertySearchFilters({
230
230
  Number of bedrooms
231
231
  </p>
232
232
  <div
233
- className="flex overflow-hidden rounded-full border-2 border-primary/40"
233
+ className="flex rounded-full border-2 border-primary"
234
234
  role="group"
235
235
  aria-label="Number of bedrooms"
236
236
  >
@@ -239,7 +239,7 @@ export default function PropertySearchFilters({
239
239
  key={value}
240
240
  type="button"
241
241
  onClick={() => onBedroomsChange(bedrooms === value ? null : value)}
242
- className={`min-w-[4rem] flex-1 border-r border-primary/40 px-4 py-2 text-sm font-medium transition-colors last:border-r-0 ${
242
+ className={`min-w-[4rem] flex-1 border-r border-primary px-4 py-2 text-sm font-medium transition-colors first:rounded-l-full last:rounded-r-full last:border-r-0 outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ${
243
243
  bedrooms === value
244
244
  ? "bg-primary text-primary-foreground"
245
245
  : "bg-background text-primary hover:bg-primary/10"
@@ -301,7 +301,7 @@ export default function PropertySearchFilters({
301
301
  key={value}
302
302
  type="button"
303
303
  onClick={() => onSortChange(value)}
304
- className={`rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors ${
304
+ className={`rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ${
305
305
  sortBy === value
306
306
  ? "bg-primary text-primary-foreground"
307
307
  : "bg-background text-foreground hover:bg-muted"
@@ -15,7 +15,7 @@ const IMPERIAL_REGIONS = new Set(["US", "LR", "MM"]);
15
15
  const MIN_TODAY_TILES = 3;
16
16
 
17
17
  /** WMO weather codes → short label (Open-Meteo) */
18
- const WEATHER_LABELS: Record<number, string> = {
18
+ export const WEATHER_LABELS: Record<number, string> = {
19
19
  0: "Clear",
20
20
  1: "Mainly clear",
21
21
  2: "Partly cloudy",
@@ -1,5 +1,12 @@
1
1
  import { useSearchParams, Link } from "react-router";
2
- import { useCallback, useState, type ChangeEvent, type SubmitEvent } from "react";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ type ChangeEvent,
8
+ type SubmitEvent,
9
+ } from "react";
3
10
  import { Button } from "../components/ui/button";
4
11
  import { Input } from "../components/ui/input";
5
12
  import { Label } from "../components/ui/label";
@@ -70,6 +77,7 @@ async function createContactForUser(): Promise<string | null> {
70
77
  function ApplicationSkeleton() {
71
78
  return (
72
79
  <div className="mx-auto max-w-[900px]" role="status">
80
+ <h1 className="sr-only">Apply for a property</h1>
73
81
  <Skeleton className="mb-4 h-4 w-28" />
74
82
 
75
83
  <Card className="mb-6 flex gap-4 rounded-2xl border border-border p-6 shadow-sm">
@@ -135,6 +143,11 @@ export default function Application() {
135
143
  const [submitting, setSubmitting] = useState(false);
136
144
  const [submitError, setSubmitError] = useState<string | null>(null);
137
145
  const [submittedName, setSubmittedName] = useState<string | null>(null);
146
+ const successHeadingRef = useRef<HTMLHeadingElement>(null);
147
+
148
+ useEffect(() => {
149
+ if (submittedName) successHeadingRef.current?.focus();
150
+ }, [submittedName]);
138
151
 
139
152
  const handleSubmit = useCallback(
140
153
  async (e: SubmitEvent<HTMLFormElement>) => {
@@ -170,9 +183,16 @@ export default function Application() {
170
183
 
171
184
  if (submittedName) {
172
185
  return (
173
- <div className="mx-auto max-w-[900px]">
186
+ <div className="mx-auto max-w-[900px]" role="status">
187
+ <h1 className="sr-only">Application submitted</h1>
174
188
  <Card className="mb-6 rounded-2xl border border-border p-6 shadow-sm">
175
- <h2 className="mb-2 text-2xl font-semibold text-foreground">Application submitted</h2>
189
+ <h2
190
+ ref={successHeadingRef}
191
+ tabIndex={-1}
192
+ className="mb-2 text-2xl font-semibold text-foreground"
193
+ >
194
+ Application submitted
195
+ </h2>
176
196
  <p className="text-sm text-muted-foreground">
177
197
  Your application
178
198
  {propertyName ? (
@@ -208,6 +228,7 @@ export default function Application() {
208
228
 
209
229
  return (
210
230
  <div className="mx-auto max-w-[900px]">
231
+ <h1 className="sr-only">Apply for a property</h1>
211
232
  <div className="mb-4">
212
233
  <Link
213
234
  to={propertyId ? `/property/${propertyId}` : "/properties"}
@@ -226,7 +247,7 @@ export default function Application() {
226
247
  </div>
227
248
  <div className="min-w-0 flex-1">
228
249
  <h2 className="mb-1.5 text-2xl font-semibold text-foreground">
229
- {propertyName ?? "Apply for a property"}
250
+ {propertyName ?? "Application details"}
230
251
  </h2>
231
252
  <p className="text-sm text-muted-foreground">
232
253
  {propertyAddress ??
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, type SubmitEvent } from "react";
1
+ import { useState, useCallback, useEffect, useRef, type SubmitEvent } from "react";
2
2
  import { Link } from "react-router";
3
3
  import { Button } from "@/components/ui/button";
4
4
  import { Input } from "@/components/ui/input";
@@ -13,23 +13,31 @@ import { useAsyncData } from "@/hooks/useAsyncData";
13
13
  import type { UserInfo } from "@/api/leads/leadApi";
14
14
 
15
15
  function SuccessCard() {
16
+ const containerRef = useRef<HTMLDivElement>(null);
17
+ useEffect(() => {
18
+ containerRef.current?.focus();
19
+ }, []);
16
20
  return (
17
- <Card>
18
- <CardHeader>
19
- <CardTitle className="text-xl">Message sent</CardTitle>
20
- </CardHeader>
21
- <CardContent className="space-y-4">
22
- <p className="text-muted-foreground">
23
- Thank you for reaching out. We've received your message and will get back to you soon.
24
- </p>
25
- <Button asChild variant="outline">
26
- <Link to="/">Back to Home</Link>
27
- </Button>
28
- <Button asChild>
29
- <Link to="/contact">Send another message</Link>
30
- </Button>
31
- </CardContent>
32
- </Card>
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>
33
41
  );
34
42
  }
35
43
 
@@ -60,13 +68,19 @@ function ContactForm({
60
68
  return (
61
69
  <Card className="relative">
62
70
  {loading && (
63
- <div className="absolute inset-0 z-10 flex items-center justify-center rounded-[inherit] bg-muted/60">
64
- <Loader2 className="size-8 animate-spin text-muted-foreground" />
71
+ <div
72
+ role="status"
73
+ aria-live="polite"
74
+ className="absolute inset-0 z-10 flex items-center justify-center rounded-[inherit] bg-muted/60"
75
+ >
76
+ <Loader2 className="size-8 animate-spin text-muted-foreground" aria-hidden />
65
77
  <span className="sr-only">Loading contact form…</span>
66
78
  </div>
67
79
  )}
68
80
  <CardHeader>
69
- <CardTitle className="text-lg">Send a message</CardTitle>
81
+ <CardTitle role="heading" aria-level={2} className="text-lg">
82
+ Send a message
83
+ </CardTitle>
70
84
  </CardHeader>
71
85
  <CardContent>
72
86
  <form
@@ -152,7 +166,7 @@ function ContactForm({
152
166
  Something went wrong. Please try again later.
153
167
  </p>
154
168
  )}
155
- <Button type="submit" disabled={submitting} className="bg-teal-600 hover:bg-teal-700">
169
+ <Button type="submit" disabled={submitting} className="bg-teal-700 hover:bg-teal-800">
156
170
  {submitting ? "Sending…" : "Send message"}
157
171
  </Button>
158
172
  </form>
@@ -14,6 +14,7 @@ export default function Dashboard() {
14
14
 
15
15
  return (
16
16
  <div className="mx-auto flex max-w-[1100px] flex-col gap-6 lg:flex-row">
17
+ <h1 className="sr-only">Dashboard</h1>
17
18
  <div className="min-w-0 flex-1">
18
19
  <Card className="border-gray-200 p-6 shadow-sm">
19
20
  <div className="mb-6 flex items-center justify-between">
@@ -212,7 +212,7 @@ export default function Home() {
212
212
  <Button
213
213
  type="submit"
214
214
  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"
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"
216
216
  >
217
217
  Find Home
218
218
  </Button>
@@ -250,7 +250,10 @@ export default function Home() {
250
250
  amenitiesMap={amenitiesMap}
251
251
  />
252
252
  ) : (
253
- <div className="rounded-xl bg-white/60 px-6 py-10 text-center text-muted-foreground">
253
+ <div
254
+ role="status"
255
+ className="rounded-xl bg-white/60 px-6 py-10 text-center text-muted-foreground"
256
+ >
254
257
  <p>No featured properties at the moment.</p>
255
258
  <Button asChild className="mt-3">
256
259
  <Link to="/properties">Browse all properties</Link>
@@ -292,7 +295,7 @@ export default function Home() {
292
295
  href="tel:18005550120"
293
296
  className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-foreground transition-colors duration-200 hover:underline"
294
297
  >
295
- <Phone className="size-4" />
298
+ <Phone className="size-4" aria-hidden />
296
299
  Talk to an Agent 1.800.555.0120
297
300
  </a>
298
301
  </div>
@@ -357,13 +360,13 @@ export default function Home() {
357
360
  </Button>
358
361
  <Button asChild variant="outline" size="lg" className="rounded-full gap-2">
359
362
  <Link to="/contact">
360
- <MessageCircle className="size-4" />
363
+ <MessageCircle className="size-4" aria-hidden />
361
364
  Contact
362
365
  </Link>
363
366
  </Button>
364
367
  <Button asChild variant="ghost" size="lg" className="rounded-full gap-2">
365
368
  <Link to="/contact">
366
- <HelpCircle className="size-4" />
369
+ <HelpCircle className="size-4" aria-hidden />
367
370
  Help
368
371
  </Link>
369
372
  </Button>
@@ -435,14 +438,14 @@ export default function Home() {
435
438
  value={footerEmail}
436
439
  onChange={(e: ChangeEvent<HTMLInputElement>) => setFooterEmail(e.target.value)}
437
440
  disabled={newsletterSubmitting}
438
- className="max-w-xs border-teal-600 bg-teal-900/50 text-white placeholder:text-teal-300"
441
+ className="max-w-xs border-teal-600 bg-teal-900/50 text-white placeholder:text-teal-300 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-teal-800"
439
442
  aria-label="Email for updates"
440
443
  />
441
444
  <Button
442
445
  type="submit"
443
446
  size="icon"
444
447
  aria-label="Subscribe to newsletter"
445
- className="shrink-0 bg-teal-600 hover:bg-teal-700"
448
+ className="shrink-0 bg-teal-600 hover:bg-teal-700 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-teal-800"
446
449
  disabled={newsletterSubmitting}
447
450
  >
448
451
  <Send className="size-4" aria-hidden />
@@ -457,7 +460,7 @@ export default function Home() {
457
460
  ? "sr-only"
458
461
  : newsletterMessage.type === "success"
459
462
  ? "text-teal-200"
460
- : "text-red-300"
463
+ : "text-red-200"
461
464
  }
462
465
  >
463
466
  {newsletterMessage?.text ?? ""}
@@ -58,6 +58,7 @@ const PAGINATION_CONFIG: PaginationConfig = {
58
58
  function MaintenanceSkeleton() {
59
59
  return (
60
60
  <div className="mx-auto max-w-[900px]" role="status">
61
+ <h1 className="sr-only">Maintenance</h1>
61
62
  <Card className="mb-6 rounded-2xl shadow-md">
62
63
  <CardHeader>
63
64
  <Skeleton className="h-6 w-1/3" />
@@ -173,8 +174,8 @@ export default function Maintenance() {
173
174
  setPriority("Standard");
174
175
  setDateRequested(new Date().toISOString().slice(0, 10));
175
176
  setRefreshCounter((c) => c + 1);
176
- } catch (err) {
177
- setSubmitError(err instanceof Error ? err.message : "Failed to submit request");
177
+ } catch {
178
+ setSubmitError("Something went wrong. Please try again later.");
178
179
  } finally {
179
180
  setSubmitting(false);
180
181
  }
@@ -186,9 +187,12 @@ export default function Maintenance() {
186
187
 
187
188
  return (
188
189
  <div className="mx-auto max-w-[900px]">
190
+ <h1 className="sr-only">Maintenance</h1>
189
191
  <Card className="mb-6 rounded-2xl shadow-md">
190
192
  <CardHeader>
191
- <CardTitle className="text-2xl text-primary">New maintenance request</CardTitle>
193
+ <CardTitle role="heading" aria-level={2} className="text-2xl text-primary">
194
+ New maintenance request
195
+ </CardTitle>
192
196
  </CardHeader>
193
197
  <CardContent className="space-y-4">
194
198
  <form onSubmit={handleSubmit} className="space-y-4">
@@ -303,11 +307,11 @@ export default function Maintenance() {
303
307
  </div>
304
308
  {submitError && (
305
309
  <p className="text-sm text-destructive" role="alert">
306
- Something went wrong. Please try again later.
310
+ {submitError}
307
311
  </p>
308
312
  )}
309
313
  {submitSuccess && (
310
- <p className="text-sm text-green-600" role="status">
314
+ <p className="text-sm text-green-700" role="status">
311
315
  Request submitted. It will appear in the list below.
312
316
  </p>
313
317
  )}
@@ -39,6 +39,7 @@ function formatDate(val: string | null): string {
39
39
  function PropertyDetailsSkeleton() {
40
40
  return (
41
41
  <div className="mx-auto max-w-[900px]" role="status">
42
+ <h1 className="sr-only">Property details</h1>
42
43
  <Skeleton className="mb-4 h-4 w-32" />
43
44
 
44
45
  <div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
@@ -141,6 +142,7 @@ export default function PropertyDetails() {
141
142
  if (error) {
142
143
  return (
143
144
  <div className="mx-auto max-w-[900px]">
145
+ <h1 className="sr-only">Property details</h1>
144
146
  <div className="mb-4">
145
147
  <Link to="/properties" className="text-sm text-primary no-underline hover:underline">
146
148
  ← Back to listings
@@ -148,7 +150,9 @@ export default function PropertyDetails() {
148
150
  </div>
149
151
  <Card className="rounded-2xl border border-border shadow-sm">
150
152
  <CardContent className="pt-6">
151
- <p className="text-destructive">Something went wrong. Please try again later.</p>
153
+ <p className="text-destructive" role="alert">
154
+ Something went wrong. Please try again later.
155
+ </p>
152
156
  </CardContent>
153
157
  </Card>
154
158
  </div>
@@ -158,6 +162,7 @@ export default function PropertyDetails() {
158
162
  if (!property && id) {
159
163
  return (
160
164
  <div className="mx-auto max-w-[900px]">
165
+ <h1 className="sr-only">Property details</h1>
161
166
  <div className="mb-4">
162
167
  <Link to="/properties" className="text-sm text-primary no-underline hover:underline">
163
168
  ← Back to listings
@@ -165,7 +170,9 @@ export default function PropertyDetails() {
165
170
  </div>
166
171
  <Card className="rounded-2xl border border-border shadow-sm">
167
172
  <CardContent className="pt-6">
168
- <p className="text-destructive">Listing not found.</p>
173
+ <p className="text-destructive" role="status">
174
+ Listing not found.
175
+ </p>
169
176
  </CardContent>
170
177
  </Card>
171
178
  </div>
@@ -253,25 +260,25 @@ export default function PropertyDetails() {
253
260
  <span className="text-xl font-semibold text-primary-foreground">
254
261
  {property?.Bedrooms__c?.value ?? "—"}
255
262
  </span>
256
- <span className="text-xs text-primary-foreground/90">Bedrooms</span>
263
+ <span className="text-xs text-primary-foreground">Bedrooms</span>
257
264
  </div>
258
265
  <div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
259
266
  <span className="text-xl font-semibold text-primary-foreground">
260
267
  {property?.Bathrooms__c?.value ?? "—"}
261
268
  </span>
262
- <span className="text-xs text-primary-foreground/90">Baths</span>
269
+ <span className="text-xs text-primary-foreground">Baths</span>
263
270
  </div>
264
271
  <div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
265
272
  <span className="text-xl font-semibold text-primary-foreground">
266
273
  {property?.Sq_Ft__c?.value ?? "—"}
267
274
  </span>
268
- <span className="text-xs text-primary-foreground/90">Square Feet</span>
275
+ <span className="text-xs text-primary-foreground">Square Feet</span>
269
276
  </div>
270
277
  <div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
271
278
  <span className="text-xl font-semibold text-primary-foreground">
272
279
  {listing?.Listing_Status__c?.value ?? "Now"}
273
280
  </span>
274
- <span className="text-xs text-primary-foreground/90">Available</span>
281
+ <span className="text-xs text-primary-foreground">Available</span>
275
282
  </div>
276
283
  </div>
277
284
  {property?.Type__c?.value && (
@@ -287,7 +294,9 @@ export default function PropertyDetails() {
287
294
  {costs.length > 0 && (
288
295
  <Card className="mb-4 rounded-2xl border border-border shadow-sm">
289
296
  <CardHeader>
290
- <CardTitle className="text-base font-semibold">Related costs</CardTitle>
297
+ <CardTitle role="heading" aria-level={2} className="text-base font-semibold">
298
+ Related costs
299
+ </CardTitle>
291
300
  </CardHeader>
292
301
  <CardContent>
293
302
  <ul className="space-y-2">
@@ -322,7 +331,9 @@ export default function PropertyDetails() {
322
331
  {features.length > 0 && (
323
332
  <Card className="mb-4 rounded-2xl border border-border shadow-sm">
324
333
  <CardHeader>
325
- <CardTitle className="text-base font-semibold">Features & amenities</CardTitle>
334
+ <CardTitle role="heading" aria-level={2} className="text-base font-semibold">
335
+ Features & amenities
336
+ </CardTitle>
326
337
  </CardHeader>
327
338
  <CardContent>
328
339
  <div className="flex flex-wrap gap-1.5">
@@ -339,6 +339,7 @@ export default function PropertySearch() {
339
339
 
340
340
  return (
341
341
  <div className="flex h-[calc(100vh-4rem)] min-h-[500px] flex-col">
342
+ <h1 className="sr-only">Property Search</h1>
342
343
  <PropertySearchFilters
343
344
  searchQuery={searchQuery}
344
345
  onSearchQueryChange={setSearchQuery}
@@ -380,7 +381,12 @@ export default function PropertySearch() {
380
381
  {searchQuery.trim() ? ` matching "${searchQuery.trim()}"` : ""}
381
382
  </h2>
382
383
  <div className="flex flex-wrap items-center gap-2">
383
- <div className="text-sm text-muted-foreground">
384
+ <div
385
+ role="status"
386
+ aria-live="polite"
387
+ aria-atomic="true"
388
+ className="text-sm text-muted-foreground"
389
+ >
384
390
  {apiUnavailable ? (
385
391
  "Placeholder (API unavailable)"
386
392
  ) : resultsLoading ? (
@@ -402,7 +408,12 @@ export default function PropertySearch() {
402
408
  )}
403
409
  </div>
404
410
  </div>
405
- <div className="flex-1 overflow-y-auto p-4">
411
+ <div
412
+ className="flex-1 overflow-y-auto p-4"
413
+ tabIndex={0}
414
+ role="region"
415
+ aria-label="Property listings"
416
+ >
406
417
  {apiUnavailable ? (
407
418
  <PropertySearchPlaceholder message="Search is temporarily unavailable." />
408
419
  ) : resultsLoading ? (
@@ -412,12 +423,12 @@ export default function PropertySearch() {
412
423
  ))}
413
424
  </div>
414
425
  ) : results.length === 0 ? (
415
- <div className="py-12 text-center">
426
+ <div role="status" className="py-12 text-center">
416
427
  <p className="mb-2 font-medium">No results found</p>
417
428
  <p className="text-sm text-muted-foreground">Try adjusting search or filters</p>
418
429
  </div>
419
430
  ) : visibleResults.length === 0 && mapBounds != null ? (
420
- <div className="py-12 text-center">
431
+ <div role="status" className="py-12 text-center">
421
432
  <p className="mb-2 font-medium">No listings in this map area</p>
422
433
  <p className="text-sm text-muted-foreground">
423
434
  Pan or zoom to see results, or clear the map filter
@@ -154,7 +154,7 @@
154
154
  --accent-foreground: oklch(0.21 0.02 260);
155
155
  /* Border / input: soft teal-tinted gray */
156
156
  --border: oklch(0.91 0.02 192);
157
- --input: oklch(0.91 0.02 192);
157
+ --input: oklch(0.62 0.03 192);
158
158
  --ring: oklch(0.52 0.09 192);
159
159
  /* Slightly larger radius for soft, friendly feel (screenshots use 16–24px) */
160
160
  --radius: 0.75rem;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
3
- "version": "11.56.2",
3
+ "version": "11.56.4",
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": "11.56.2",
9
+ "version": "11.56.4",
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": "11.56.2",
3
+ "version": "11.56.4",
4
4
  "description": "Base SFDX project template",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "publishConfig": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-app-react-sample-b2x",
3
- "version": "11.56.2",
3
+ "version": "11.56.4",
4
4
  "description": "Salesforce sample property rental React app",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",