react-listing-engine 0.1.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 (51) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +161 -0
  4. package/dist/chunk-2T5PE5MB.js +2 -0
  5. package/dist/chunk-2VUHLHHX.cjs +2 -0
  6. package/dist/chunk-35KYEBAC.js +2 -0
  7. package/dist/chunk-5FBI2WIM.js +2 -0
  8. package/dist/chunk-5O36WI7H.cjs +2 -0
  9. package/dist/chunk-6PZHSHU3.js +2 -0
  10. package/dist/chunk-CHNUE46K.cjs +2 -0
  11. package/dist/chunk-LCQZIWBO.cjs +2 -0
  12. package/dist/chunk-N3OZ2HK2.cjs +2 -0
  13. package/dist/chunk-PFPHWJAA.js +2 -0
  14. package/dist/components-provider-CF0Mo0B8.d.cts +120 -0
  15. package/dist/components-provider-CjMxkxrP.d.ts +120 -0
  16. package/dist/entity-adapter.interface-BDgbSxhq.d.cts +33 -0
  17. package/dist/entity-adapter.interface-BDgbSxhq.d.ts +33 -0
  18. package/dist/index.cjs +2 -0
  19. package/dist/index.d.cts +674 -0
  20. package/dist/index.d.ts +674 -0
  21. package/dist/index.js +2 -0
  22. package/dist/listing-app-De6OqpUC.d.ts +199 -0
  23. package/dist/listing-app-pKMQBuQI.d.cts +199 -0
  24. package/dist/listing-config-options.interface-ZJYY_ZvR.d.cts +12 -0
  25. package/dist/listing-config-options.interface-ZJYY_ZvR.d.ts +12 -0
  26. package/dist/map-provider.interface-BMnwW3ob.d.cts +37 -0
  27. package/dist/map-provider.interface-BxexMT3X.d.ts +37 -0
  28. package/dist/maps/google/index.cjs +2 -0
  29. package/dist/maps/google/index.d.cts +39 -0
  30. package/dist/maps/google/index.d.ts +39 -0
  31. package/dist/maps/google/index.js +2 -0
  32. package/dist/presets/rental/index.cjs +2 -0
  33. package/dist/presets/rental/index.d.cts +266 -0
  34. package/dist/presets/rental/index.d.ts +266 -0
  35. package/dist/presets/rental/index.js +2 -0
  36. package/dist/shadcn/index.cjs +2 -0
  37. package/dist/shadcn/index.d.cts +261 -0
  38. package/dist/shadcn/index.d.ts +261 -0
  39. package/dist/shadcn/index.js +2 -0
  40. package/dist/styled/index.cjs +2 -0
  41. package/dist/styled/index.d.cts +123 -0
  42. package/dist/styled/index.d.ts +123 -0
  43. package/dist/styled/index.js +2 -0
  44. package/dist/styles.css +972 -0
  45. package/dist/testing/index.cjs +2 -0
  46. package/dist/testing/index.d.cts +51 -0
  47. package/dist/testing/index.d.ts +51 -0
  48. package/dist/testing/index.js +2 -0
  49. package/dist/url-sync.controller-CsU_QxoS.d.cts +89 -0
  50. package/dist/url-sync.controller-DX67JivW.d.ts +89 -0
  51. package/package.json +143 -0
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ import{a as O,p as m}from"./chunk-2T5PE5MB.js";import{A as N,B as U,a as y,b as P,c as L,d as h,e as b,f as x,g as C,h as I,i as k,j as q,k as v,l as B,m as S,n as A,o as F,p as T,q as p,r as w,s,t as i,u as H,v as z,w as D,x as G,y as J,z as K}from"./chunk-PFPHWJAA.js";import{a as R}from"./chunk-5FBI2WIM.js";import{a as Q}from"./chunk-35KYEBAC.js";var d=(o=>(o.Paged="paged",o.Infinite="infinite",o))(d||{});var f=(r=>(r.FiltersChanged="FiltersChanged",r.ResultsLoaded="ResultsLoaded",r.PointClicked="PointClicked",r.BoundsChanged="BoundsChanged",r.LayerToggled="LayerToggled",r))(f||{});var u=class{query;listeners=new Set;constructor(t={}){this.query={...t}}getQuery(){return{...this.query}}setQuery(t){this.query={...t},this.notify()}subscribe(t){return this.listeners.add(t),()=>{this.listeners.delete(t)}}notify(){for(let t of[...this.listeners])t()}};import{jsx as c}from"react/jsx-runtime";function Z({children:e}){let{Toolbar:t}=p();return c(t,{children:e})}import{useCallback as l}from"react";function tt(){let e=s(),t=i(),o=l(a=>e.loadPoints(a),[e]),n=l((a,r)=>e.selectPoint(a,r),[e]);return{bounds:t.bounds,points:t.points,loadPoints:o,selectPoint:n}}import{useCallback as g}from"react";function it(e){let t=s(),o=i(),n=g(()=>t.toggleLayer(e),[t,e]);return{visible:o.layers[e]??!0,points:o.points[e]??[],toggle:n}}export{R as BrowserHistoryPort,L as DatasetRegistry,P as FilterRegistry,m as ListingApp,T as ListingComponentsProvider,x as ListingConfig,C as ListingEngine,w as ListingEngineContext,f as ListingEventType,z as ListingFilters,G as ListingList,J as ListingMap,K as ListingPagination,U as ListingProvider,N as ListingResultHeader,y as ListingStore,Z as ListingToolbar,u as MemoryHistoryPort,d as PaginationMode,h as TypedEmitter,Q as UrlSyncController,I as composeListingProviders,b as listingDefaultConfig,s as useListing,p as useListingComponents,O as useListingEvent,H as useListingFilters,it as useListingLayer,tt as useListingMap,D as useListingResults,i as useListingState,k as withConfig,v as withDataset,B as withFilters,A as withInitialFilters,q as withMap,F as withPrimaryDataset,S as withUrlSync};
@@ -0,0 +1,199 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { L as LatLng } from './entity-adapter.interface-BDgbSxhq.js';
4
+ import { M as MapProvider } from './map-provider.interface-BxexMT3X.js';
5
+ import { D as DatasetDefinition, F as FilterRegistry, d as IListingComponents } from './components-provider-CjMxkxrP.js';
6
+ import { I as IListingConfigOptions } from './listing-config-options.interface-ZJYY_ZvR.js';
7
+
8
+ type BottomNavView = 'list' | 'map';
9
+ interface IBottomNavAction {
10
+ label: string;
11
+ icon?: ReactNode;
12
+ onClick(): void;
13
+ }
14
+ interface IBottomNavProps {
15
+ view: BottomNavView;
16
+ onViewChange(view: BottomNavView): void;
17
+ onFiltersClick(): void;
18
+ action?: IBottomNavAction;
19
+ }
20
+ /**
21
+ * Mobile-only bottom navigation bar (`.rle-bottom-nav`, hidden at 768px+ by
22
+ * CSS -- see `styles.css`): a **Filters** button that opens the mobile
23
+ * `BottomSheet`, an optional caller-supplied `action` button (e.g. "Add"),
24
+ * and a **List | Map** segmented toggle (`.rle-viewtoggle`) that drives which
25
+ * of `StyledListingLayout`'s two full-area panels is visible. No lucide/icon
26
+ * library dependency -- every icon here is a small inline SVG, matching the
27
+ * rest of `/styled`'s zero-extra-dependency policy.
28
+ */
29
+ declare function BottomNav({ view, onViewChange, onFiltersClick, action }: IBottomNavProps): react.JSX.Element;
30
+
31
+ interface IStyledListingLayoutProps {
32
+ /** Optional search box -- same shape/reasoning as `/shadcn`'s `ListingLayout.search`; omitted entirely when not passed. */
33
+ search?: {
34
+ value: string;
35
+ onChange: (value: string) => void;
36
+ placeholder?: string;
37
+ };
38
+ /** Extra content rendered at the end of the desktop filter bar, alongside `ListingResultHeader` (e.g. a sort control). */
39
+ toolbarEnd?: ReactNode;
40
+ /** Optional bottom-nav action button (e.g. "Add"), forwarded verbatim to `<BottomNav action={...} />`. Omit to render just Filters + the List|Map toggle. */
41
+ mobileAction?: IBottomNavAction;
42
+ /** Whether the layout fetches the first page itself on mount (`engine.applyFilters({})`). Defaults to `true`. */
43
+ autoFetch?: boolean;
44
+ /** Forwarded verbatim to `<ListingMap center={mapCenter} />` -- see that component's "Auto-fit" doc comment. */
45
+ mapCenter?: LatLng;
46
+ /** Forwarded verbatim to `<ListingMap zoom={mapZoom} />`. */
47
+ mapZoom?: number;
48
+ className?: string;
49
+ }
50
+ /**
51
+ * Full, responsive, Tailwind-free listing experience -- the `/styled`
52
+ * counterpart to `/shadcn`'s `ListingLayout`, built from the same
53
+ * structure-only compound components (`~/react`) plus the injected
54
+ * `Styled*` slot components, but with a richer mobile experience: a bottom
55
+ * nav (`BottomNav`) and a bottom sheet (`BottomSheet`) for filters, instead
56
+ * of `/shadcn`'s inline mobile toggle bar. Every class used here is one of
57
+ * the `.rle-*` layout classes added to `styles.css` alongside this file --
58
+ * no Tailwind, no inline styles, no other stylesheet required.
59
+ *
60
+ * STRUCTURE (`.rle-app`):
61
+ * - `.rle-filter-bar` (desktop only, hidden below 768px by CSS): the
62
+ * optional `Search` slot, `<ListingFilters>` laid out as a wrapping row
63
+ * (`className="rle-filters-row"`, `groupClassName="rle-filter-group"`),
64
+ * and a trailing `.rle-filter-bar__end` cluster with `<ListingResultHeader>`
65
+ * + `toolbarEnd`.
66
+ * - `.rle-body.rle-split`: a CSS grid from 768px up (list column floors at
67
+ * 340px, caps at 42%; map takes the rest); below that, a single full-area
68
+ * panel with exactly one of `.rle-list`/`.rle-map` visible at a time via
69
+ * `data-mobile-view` (see `styles.css`'s mobile media query). Both regions
70
+ * stay mounted at ALL times regardless of viewport/toggle state -- only
71
+ * their visibility flips -- so neither `ListingList` nor `ListingMap`
72
+ * remounts (and re-triggers its own mount effects) on toggle or resize.
73
+ * - `<BottomNav>`: mobile-only (CSS-hidden at 768px+). Wired to the same
74
+ * `mobileView` state as the CSS toggle, `mobileAction`, and opens the
75
+ * filters `<BottomSheet>`.
76
+ * - `<BottomSheet title="Filters">`: the SAME `<ListingFilters>` component,
77
+ * stacked vertically (`className="rle-filter-stack"`) for the sheet's
78
+ * narrower body, plus a footer with "Clear all" (resets every registered
79
+ * filter to `undefined` via `engine.filters.list()`'s keys, routed through
80
+ * `engine.applyFilters` -- the one bulk mutator, same as `ListingFilters`
81
+ * itself uses) and "Show N results" (just closes the sheet -- every filter
82
+ * control already applies live via its own `onChange`, so there is nothing
83
+ * left to commit).
84
+ * - Fetches the first page itself on mount (`engine.applyFilters({})`) by
85
+ * default, exactly like `/shadcn`'s `ListingLayout` -- pass `autoFetch={false}`
86
+ * to opt out and drive the first fetch yourself.
87
+ */
88
+ declare function StyledListingLayout({ search, toolbarEnd, mobileAction, autoFetch, mapCenter, mapZoom, className, }: IStyledListingLayoutProps): react.JSX.Element;
89
+
90
+ /**
91
+ * Same two-shape `map` prop as `/shadcn`'s `ListingApp` (see that file's
92
+ * `useResolvedMap` doc comment for why the `{ apiKey }` shorthand is resolved
93
+ * via a dynamic `import()` rather than a static one -- the reasoning is
94
+ * identical here). Re-declared rather than imported from `~/shadcn`: the two
95
+ * styled adapters are SIBLINGS, not a hierarchy -- `/styled` never depends on
96
+ * `/shadcn` (or vice versa), each is self-contained against `~/core`/`~/react`.
97
+ */
98
+ type ListingAppMapProp = {
99
+ provider: MapProvider;
100
+ center?: LatLng;
101
+ zoom?: number;
102
+ } | {
103
+ apiKey: string;
104
+ mapId?: string;
105
+ center?: LatLng;
106
+ zoom?: number;
107
+ };
108
+ interface ListingAppProps<TFilters> {
109
+ /**
110
+ * One or more marker-layer datasets; the FIRST entry is the primary
111
+ * dataset (drives the results list + pagination) -- same "insertion
112
+ * order" rule `composeListingProviders`/`withDataset` already follow.
113
+ * Entity-erased (`any`, not `TEntity`) for the same reason
114
+ * `DatasetRegistry<unknown, TFilters>` is: one array can legitimately hold
115
+ * heterogeneous layers (a properties dataset and a businesses dataset have
116
+ * different entity types) -- see `compose-listing-providers.ts`'s
117
+ * `withDataset` doc comment.
118
+ */
119
+ datasets: DatasetDefinition<any, TFilters>[];
120
+ /** e.g. `withRentalFilters()`, or any `(reg) => { reg.add(...); }` callback -- forwarded verbatim to `withFilters`. */
121
+ filters?: (reg: FilterRegistry<TFilters>) => void;
122
+ /** A ready `MapProvider`, or `{ apiKey, mapId? }` to build a `googleProvider` internally. Omit for no map (the styled layout shows a "Map unavailable" fallback). */
123
+ map?: ListingAppMapProp;
124
+ /** Slot overrides, merged OVER the `/styled` defaults -- see this file's doc comment for how the merge works. */
125
+ components?: Partial<IListingComponents>;
126
+ /**
127
+ * Hydrates the engine's initial filters on mount -- typically read from
128
+ * the CONSUMER's own URL (e.g. `rentalFiltersFromQuery(new
129
+ * URLSearchParams(window.location.search))`), but any source works. Half
130
+ * of the event-based URL API this component exposes: `ListingApp` never
131
+ * reads `window.location` itself, it only accepts filters as a prop.
132
+ */
133
+ initialFilters?: TFilters;
134
+ /**
135
+ * EVENT OUT: fires with the engine's current filters (`TFilters`, not the
136
+ * store's `DeepReadonly` wrapper) every time `ListingEventType.FiltersChanged`
137
+ * fires on the underlying engine -- i.e. on every `engine.applyFilters(...)`
138
+ * call, INCLUDING the one `StyledListingLayout` makes on mount when
139
+ * `autoFetch` is on (a `{}` patch that still round-trips through
140
+ * `currentFilters()` and re-emits the very `initialFilters` this component
141
+ * was given). That first call is intentionally NOT suppressed: a
142
+ * consumer's handler is expected to write filters back to a URL via
143
+ * `history.replaceState` (or a router's equivalent), and replacing the URL
144
+ * with the SAME query string it already hydrated from is a no-op, not a
145
+ * visible echo -- so "just always emit" is simpler than tracking a
146
+ * first-call flag for no behavioral gain. This is the OTHER half of the
147
+ * event-based URL API: `ListingApp` (and the engine underneath it) never
148
+ * touches `window.history` -- the CONSUMER owns routing, driven by this
149
+ * callback plus `initialFilters` above. `UrlSyncController`/`BrowserHistoryPort`
150
+ * (`~/core`, `~/react`) remain exported as optional, lower-level helpers
151
+ * for consumers who want the library to own `window.history` directly, but
152
+ * they are no longer the primary/recommended path for new integrations.
153
+ */
154
+ onFiltersChange?: (filters: TFilters) => void;
155
+ /** The mobile bottom-nav "Add" action -- forwarded verbatim to `StyledListingLayout`/`BottomNav`. */
156
+ mobileAction?: IBottomNavAction;
157
+ search?: IStyledListingLayoutProps['search'];
158
+ toolbarEnd?: IStyledListingLayoutProps['toolbarEnd'];
159
+ config?: Partial<IListingConfigOptions>;
160
+ autoFetch?: boolean;
161
+ className?: string;
162
+ }
163
+ /**
164
+ * Turnkey, batteries-included, Tailwind-free entry point -- the `/styled`
165
+ * counterpart to `/shadcn`'s `ListingApp`, and (per `src/index.ts`) the
166
+ * package's MAIN-ENTRY default: `import { ListingApp } from
167
+ * 'react-listing-engine'` gives you this component. Pass datasets + filters +
168
+ * a map + component overrides, and `ListingApp` composes
169
+ * `composeListingProviders(...)`, `<ListingProvider>`, the `/styled` defaults,
170
+ * and `<StyledListingLayout>` for you.
171
+ *
172
+ * `TEntity` still can't be inferred from these props (`datasets` narrows it,
173
+ * but a multi-dataset array widens back to the union/`unknown` in practice)
174
+ * -- annotate the call site when entity-level typing matters, exactly like
175
+ * `<ListingProvider<TEntity, TFilters>>` itself.
176
+ *
177
+ * COMPONENTS MERGE: `components` overrides are applied ON TOP of the
178
+ * `/styled` defaults via ONE explicit `{ ...styledDefaultComponents,
179
+ * ...components }` object, passed to a single `<ListingComponentsProvider>`
180
+ * -- never as a nested `<ListingComponentsProvider>` inside
181
+ * `StyledComponentsProviderWithDefaults`. `ListingComponentsProvider` merges
182
+ * `provided ?? ITS OWN private, unstyled fallbacks` per slot (it does not
183
+ * read the parent context -- see `src/react/components-provider.tsx`), so
184
+ * nesting would silently discard every un-overridden `/styled` default
185
+ * instead of keeping it.
186
+ *
187
+ * URL SYNC IS EVENT-BASED, NOT INTERNAL: unlike `/shadcn`'s `ListingApp`,
188
+ * this component takes no `urlSync` prop and never constructs/starts a
189
+ * `UrlSyncController` -- it never touches `window.history`. Instead it
190
+ * accepts `initialFilters` (hydrate FROM the consumer's URL) and emits
191
+ * `onFiltersChange` (write TO the consumer's URL/router) -- see both props'
192
+ * doc comments for the full contract. This keeps the library's surface
193
+ * router-agnostic (a plain `history.replaceState`, a Next.js `router.replace`,
194
+ * a React Router `setSearchParams`, etc. all work identically from the
195
+ * consumer's side) instead of assuming `window.history` is the right target.
196
+ */
197
+ declare function ListingApp<TEntity, TFilters>(props: ListingAppProps<TFilters>): react.JSX.Element | null;
198
+
199
+ export { BottomNav as B, type IBottomNavAction as I, ListingApp as L, StyledListingLayout as S, type ListingAppProps as a, type BottomNavView as b, type IBottomNavProps as c, type IStyledListingLayoutProps as d, type ListingAppMapProp as e };
@@ -0,0 +1,199 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { L as LatLng } from './entity-adapter.interface-BDgbSxhq.cjs';
4
+ import { M as MapProvider } from './map-provider.interface-BMnwW3ob.cjs';
5
+ import { D as DatasetDefinition, F as FilterRegistry, d as IListingComponents } from './components-provider-CF0Mo0B8.cjs';
6
+ import { I as IListingConfigOptions } from './listing-config-options.interface-ZJYY_ZvR.cjs';
7
+
8
+ type BottomNavView = 'list' | 'map';
9
+ interface IBottomNavAction {
10
+ label: string;
11
+ icon?: ReactNode;
12
+ onClick(): void;
13
+ }
14
+ interface IBottomNavProps {
15
+ view: BottomNavView;
16
+ onViewChange(view: BottomNavView): void;
17
+ onFiltersClick(): void;
18
+ action?: IBottomNavAction;
19
+ }
20
+ /**
21
+ * Mobile-only bottom navigation bar (`.rle-bottom-nav`, hidden at 768px+ by
22
+ * CSS -- see `styles.css`): a **Filters** button that opens the mobile
23
+ * `BottomSheet`, an optional caller-supplied `action` button (e.g. "Add"),
24
+ * and a **List | Map** segmented toggle (`.rle-viewtoggle`) that drives which
25
+ * of `StyledListingLayout`'s two full-area panels is visible. No lucide/icon
26
+ * library dependency -- every icon here is a small inline SVG, matching the
27
+ * rest of `/styled`'s zero-extra-dependency policy.
28
+ */
29
+ declare function BottomNav({ view, onViewChange, onFiltersClick, action }: IBottomNavProps): react.JSX.Element;
30
+
31
+ interface IStyledListingLayoutProps {
32
+ /** Optional search box -- same shape/reasoning as `/shadcn`'s `ListingLayout.search`; omitted entirely when not passed. */
33
+ search?: {
34
+ value: string;
35
+ onChange: (value: string) => void;
36
+ placeholder?: string;
37
+ };
38
+ /** Extra content rendered at the end of the desktop filter bar, alongside `ListingResultHeader` (e.g. a sort control). */
39
+ toolbarEnd?: ReactNode;
40
+ /** Optional bottom-nav action button (e.g. "Add"), forwarded verbatim to `<BottomNav action={...} />`. Omit to render just Filters + the List|Map toggle. */
41
+ mobileAction?: IBottomNavAction;
42
+ /** Whether the layout fetches the first page itself on mount (`engine.applyFilters({})`). Defaults to `true`. */
43
+ autoFetch?: boolean;
44
+ /** Forwarded verbatim to `<ListingMap center={mapCenter} />` -- see that component's "Auto-fit" doc comment. */
45
+ mapCenter?: LatLng;
46
+ /** Forwarded verbatim to `<ListingMap zoom={mapZoom} />`. */
47
+ mapZoom?: number;
48
+ className?: string;
49
+ }
50
+ /**
51
+ * Full, responsive, Tailwind-free listing experience -- the `/styled`
52
+ * counterpart to `/shadcn`'s `ListingLayout`, built from the same
53
+ * structure-only compound components (`~/react`) plus the injected
54
+ * `Styled*` slot components, but with a richer mobile experience: a bottom
55
+ * nav (`BottomNav`) and a bottom sheet (`BottomSheet`) for filters, instead
56
+ * of `/shadcn`'s inline mobile toggle bar. Every class used here is one of
57
+ * the `.rle-*` layout classes added to `styles.css` alongside this file --
58
+ * no Tailwind, no inline styles, no other stylesheet required.
59
+ *
60
+ * STRUCTURE (`.rle-app`):
61
+ * - `.rle-filter-bar` (desktop only, hidden below 768px by CSS): the
62
+ * optional `Search` slot, `<ListingFilters>` laid out as a wrapping row
63
+ * (`className="rle-filters-row"`, `groupClassName="rle-filter-group"`),
64
+ * and a trailing `.rle-filter-bar__end` cluster with `<ListingResultHeader>`
65
+ * + `toolbarEnd`.
66
+ * - `.rle-body.rle-split`: a CSS grid from 768px up (list column floors at
67
+ * 340px, caps at 42%; map takes the rest); below that, a single full-area
68
+ * panel with exactly one of `.rle-list`/`.rle-map` visible at a time via
69
+ * `data-mobile-view` (see `styles.css`'s mobile media query). Both regions
70
+ * stay mounted at ALL times regardless of viewport/toggle state -- only
71
+ * their visibility flips -- so neither `ListingList` nor `ListingMap`
72
+ * remounts (and re-triggers its own mount effects) on toggle or resize.
73
+ * - `<BottomNav>`: mobile-only (CSS-hidden at 768px+). Wired to the same
74
+ * `mobileView` state as the CSS toggle, `mobileAction`, and opens the
75
+ * filters `<BottomSheet>`.
76
+ * - `<BottomSheet title="Filters">`: the SAME `<ListingFilters>` component,
77
+ * stacked vertically (`className="rle-filter-stack"`) for the sheet's
78
+ * narrower body, plus a footer with "Clear all" (resets every registered
79
+ * filter to `undefined` via `engine.filters.list()`'s keys, routed through
80
+ * `engine.applyFilters` -- the one bulk mutator, same as `ListingFilters`
81
+ * itself uses) and "Show N results" (just closes the sheet -- every filter
82
+ * control already applies live via its own `onChange`, so there is nothing
83
+ * left to commit).
84
+ * - Fetches the first page itself on mount (`engine.applyFilters({})`) by
85
+ * default, exactly like `/shadcn`'s `ListingLayout` -- pass `autoFetch={false}`
86
+ * to opt out and drive the first fetch yourself.
87
+ */
88
+ declare function StyledListingLayout({ search, toolbarEnd, mobileAction, autoFetch, mapCenter, mapZoom, className, }: IStyledListingLayoutProps): react.JSX.Element;
89
+
90
+ /**
91
+ * Same two-shape `map` prop as `/shadcn`'s `ListingApp` (see that file's
92
+ * `useResolvedMap` doc comment for why the `{ apiKey }` shorthand is resolved
93
+ * via a dynamic `import()` rather than a static one -- the reasoning is
94
+ * identical here). Re-declared rather than imported from `~/shadcn`: the two
95
+ * styled adapters are SIBLINGS, not a hierarchy -- `/styled` never depends on
96
+ * `/shadcn` (or vice versa), each is self-contained against `~/core`/`~/react`.
97
+ */
98
+ type ListingAppMapProp = {
99
+ provider: MapProvider;
100
+ center?: LatLng;
101
+ zoom?: number;
102
+ } | {
103
+ apiKey: string;
104
+ mapId?: string;
105
+ center?: LatLng;
106
+ zoom?: number;
107
+ };
108
+ interface ListingAppProps<TFilters> {
109
+ /**
110
+ * One or more marker-layer datasets; the FIRST entry is the primary
111
+ * dataset (drives the results list + pagination) -- same "insertion
112
+ * order" rule `composeListingProviders`/`withDataset` already follow.
113
+ * Entity-erased (`any`, not `TEntity`) for the same reason
114
+ * `DatasetRegistry<unknown, TFilters>` is: one array can legitimately hold
115
+ * heterogeneous layers (a properties dataset and a businesses dataset have
116
+ * different entity types) -- see `compose-listing-providers.ts`'s
117
+ * `withDataset` doc comment.
118
+ */
119
+ datasets: DatasetDefinition<any, TFilters>[];
120
+ /** e.g. `withRentalFilters()`, or any `(reg) => { reg.add(...); }` callback -- forwarded verbatim to `withFilters`. */
121
+ filters?: (reg: FilterRegistry<TFilters>) => void;
122
+ /** A ready `MapProvider`, or `{ apiKey, mapId? }` to build a `googleProvider` internally. Omit for no map (the styled layout shows a "Map unavailable" fallback). */
123
+ map?: ListingAppMapProp;
124
+ /** Slot overrides, merged OVER the `/styled` defaults -- see this file's doc comment for how the merge works. */
125
+ components?: Partial<IListingComponents>;
126
+ /**
127
+ * Hydrates the engine's initial filters on mount -- typically read from
128
+ * the CONSUMER's own URL (e.g. `rentalFiltersFromQuery(new
129
+ * URLSearchParams(window.location.search))`), but any source works. Half
130
+ * of the event-based URL API this component exposes: `ListingApp` never
131
+ * reads `window.location` itself, it only accepts filters as a prop.
132
+ */
133
+ initialFilters?: TFilters;
134
+ /**
135
+ * EVENT OUT: fires with the engine's current filters (`TFilters`, not the
136
+ * store's `DeepReadonly` wrapper) every time `ListingEventType.FiltersChanged`
137
+ * fires on the underlying engine -- i.e. on every `engine.applyFilters(...)`
138
+ * call, INCLUDING the one `StyledListingLayout` makes on mount when
139
+ * `autoFetch` is on (a `{}` patch that still round-trips through
140
+ * `currentFilters()` and re-emits the very `initialFilters` this component
141
+ * was given). That first call is intentionally NOT suppressed: a
142
+ * consumer's handler is expected to write filters back to a URL via
143
+ * `history.replaceState` (or a router's equivalent), and replacing the URL
144
+ * with the SAME query string it already hydrated from is a no-op, not a
145
+ * visible echo -- so "just always emit" is simpler than tracking a
146
+ * first-call flag for no behavioral gain. This is the OTHER half of the
147
+ * event-based URL API: `ListingApp` (and the engine underneath it) never
148
+ * touches `window.history` -- the CONSUMER owns routing, driven by this
149
+ * callback plus `initialFilters` above. `UrlSyncController`/`BrowserHistoryPort`
150
+ * (`~/core`, `~/react`) remain exported as optional, lower-level helpers
151
+ * for consumers who want the library to own `window.history` directly, but
152
+ * they are no longer the primary/recommended path for new integrations.
153
+ */
154
+ onFiltersChange?: (filters: TFilters) => void;
155
+ /** The mobile bottom-nav "Add" action -- forwarded verbatim to `StyledListingLayout`/`BottomNav`. */
156
+ mobileAction?: IBottomNavAction;
157
+ search?: IStyledListingLayoutProps['search'];
158
+ toolbarEnd?: IStyledListingLayoutProps['toolbarEnd'];
159
+ config?: Partial<IListingConfigOptions>;
160
+ autoFetch?: boolean;
161
+ className?: string;
162
+ }
163
+ /**
164
+ * Turnkey, batteries-included, Tailwind-free entry point -- the `/styled`
165
+ * counterpart to `/shadcn`'s `ListingApp`, and (per `src/index.ts`) the
166
+ * package's MAIN-ENTRY default: `import { ListingApp } from
167
+ * 'react-listing-engine'` gives you this component. Pass datasets + filters +
168
+ * a map + component overrides, and `ListingApp` composes
169
+ * `composeListingProviders(...)`, `<ListingProvider>`, the `/styled` defaults,
170
+ * and `<StyledListingLayout>` for you.
171
+ *
172
+ * `TEntity` still can't be inferred from these props (`datasets` narrows it,
173
+ * but a multi-dataset array widens back to the union/`unknown` in practice)
174
+ * -- annotate the call site when entity-level typing matters, exactly like
175
+ * `<ListingProvider<TEntity, TFilters>>` itself.
176
+ *
177
+ * COMPONENTS MERGE: `components` overrides are applied ON TOP of the
178
+ * `/styled` defaults via ONE explicit `{ ...styledDefaultComponents,
179
+ * ...components }` object, passed to a single `<ListingComponentsProvider>`
180
+ * -- never as a nested `<ListingComponentsProvider>` inside
181
+ * `StyledComponentsProviderWithDefaults`. `ListingComponentsProvider` merges
182
+ * `provided ?? ITS OWN private, unstyled fallbacks` per slot (it does not
183
+ * read the parent context -- see `src/react/components-provider.tsx`), so
184
+ * nesting would silently discard every un-overridden `/styled` default
185
+ * instead of keeping it.
186
+ *
187
+ * URL SYNC IS EVENT-BASED, NOT INTERNAL: unlike `/shadcn`'s `ListingApp`,
188
+ * this component takes no `urlSync` prop and never constructs/starts a
189
+ * `UrlSyncController` -- it never touches `window.history`. Instead it
190
+ * accepts `initialFilters` (hydrate FROM the consumer's URL) and emits
191
+ * `onFiltersChange` (write TO the consumer's URL/router) -- see both props'
192
+ * doc comments for the full contract. This keeps the library's surface
193
+ * router-agnostic (a plain `history.replaceState`, a Next.js `router.replace`,
194
+ * a React Router `setSearchParams`, etc. all work identically from the
195
+ * consumer's side) instead of assuming `window.history` is the right target.
196
+ */
197
+ declare function ListingApp<TEntity, TFilters>(props: ListingAppProps<TFilters>): react.JSX.Element | null;
198
+
199
+ export { BottomNav as B, type IBottomNavAction as I, ListingApp as L, StyledListingLayout as S, type ListingAppProps as a, type BottomNavView as b, type IBottomNavProps as c, type IStyledListingLayoutProps as d, type ListingAppMapProp as e };
@@ -0,0 +1,12 @@
1
+ declare enum PaginationMode {
2
+ Paged = "paged",
3
+ Infinite = "infinite"
4
+ }
5
+
6
+ interface IListingConfigOptions {
7
+ pagination: PaginationMode;
8
+ pageSize: number;
9
+ debounceMs: number;
10
+ }
11
+
12
+ export { type IListingConfigOptions as I, PaginationMode as P };
@@ -0,0 +1,12 @@
1
+ declare enum PaginationMode {
2
+ Paged = "paged",
3
+ Infinite = "infinite"
4
+ }
5
+
6
+ interface IListingConfigOptions {
7
+ pagination: PaginationMode;
8
+ pageSize: number;
9
+ debounceMs: number;
10
+ }
11
+
12
+ export { type IListingConfigOptions as I, PaginationMode as P };
@@ -0,0 +1,37 @@
1
+ import { L as LatLng, B as Bounds } from './entity-adapter.interface-BDgbSxhq.cjs';
2
+
3
+ /**
4
+ * Providers that need an API key take it via their factory (e.g. `googleProvider({ apiKey })`),
5
+ * not per-mount.
6
+ */
7
+ interface MapInitOptions {
8
+ apiKey?: string;
9
+ center?: LatLng;
10
+ zoom?: number;
11
+ }
12
+ interface MapHandle {
13
+ readonly raw: unknown;
14
+ }
15
+ interface RenderedLayer {
16
+ id: string;
17
+ markers: Array<{
18
+ id: string | number;
19
+ position: LatLng;
20
+ iconUrl?: string;
21
+ element?: HTMLElement;
22
+ }>;
23
+ clustering?: {
24
+ maxZoom?: number;
25
+ } | false;
26
+ onMarkerClick?(id: string | number): void;
27
+ }
28
+ type Unsubscribe = () => void;
29
+ interface MapProvider {
30
+ mount(el: HTMLElement, opts: MapInitOptions): Promise<MapHandle> | MapHandle;
31
+ renderLayer(handle: MapHandle, layer: RenderedLayer): Unsubscribe;
32
+ onBoundsChange(handle: MapHandle, cb: (b: Bounds) => void): Unsubscribe;
33
+ fitBounds(handle: MapHandle, b: Bounds): void;
34
+ destroy(handle: MapHandle): void;
35
+ }
36
+
37
+ export type { MapProvider as M, RenderedLayer as R, Unsubscribe as U, MapHandle as a, MapInitOptions as b };
@@ -0,0 +1,37 @@
1
+ import { L as LatLng, B as Bounds } from './entity-adapter.interface-BDgbSxhq.js';
2
+
3
+ /**
4
+ * Providers that need an API key take it via their factory (e.g. `googleProvider({ apiKey })`),
5
+ * not per-mount.
6
+ */
7
+ interface MapInitOptions {
8
+ apiKey?: string;
9
+ center?: LatLng;
10
+ zoom?: number;
11
+ }
12
+ interface MapHandle {
13
+ readonly raw: unknown;
14
+ }
15
+ interface RenderedLayer {
16
+ id: string;
17
+ markers: Array<{
18
+ id: string | number;
19
+ position: LatLng;
20
+ iconUrl?: string;
21
+ element?: HTMLElement;
22
+ }>;
23
+ clustering?: {
24
+ maxZoom?: number;
25
+ } | false;
26
+ onMarkerClick?(id: string | number): void;
27
+ }
28
+ type Unsubscribe = () => void;
29
+ interface MapProvider {
30
+ mount(el: HTMLElement, opts: MapInitOptions): Promise<MapHandle> | MapHandle;
31
+ renderLayer(handle: MapHandle, layer: RenderedLayer): Unsubscribe;
32
+ onBoundsChange(handle: MapHandle, cb: (b: Bounds) => void): Unsubscribe;
33
+ fitBounds(handle: MapHandle, b: Bounds): void;
34
+ destroy(handle: MapHandle): void;
35
+ }
36
+
37
+ export type { MapProvider as M, RenderedLayer as R, Unsubscribe as U, MapHandle as a, MapInitOptions as b };
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
2
+ var _jsapiloader = require('@googlemaps/js-api-loader');var y="DEMO_MAP_ID";function u(r){return r.raw}function v(r,n){if(r.element)return r.element;if(r.iconUrl)return new n.PinElement({glyphSrc:r.iconUrl})}function p(r){for(let n of r)n.map=null}var L="#b3261e",b=40,C=64,w=14;function E(r,n){let s=n.count,o=Math.min(C,b+Math.round(Math.log10(s+1)*w)),e=document.createElement("div");return e.textContent=String(s),e.style.display="flex",e.style.alignItems="center",e.style.justifyContent="center",e.style.width=`${o}px`,e.style.height=`${o}px`,e.style.borderRadius="50%",e.style.backgroundColor=L,e.style.border="2px solid #ffffff",e.style.boxShadow="0 1px 4px rgba(0, 0, 0, 0.4)",e.style.color="#ffffff",e.style.fontFamily="inherit",e.style.fontWeight="700",e.style.fontSize=o>=52?"15px":"13px",new r.AdvancedMarkerElement({position:n.position,content:e})}var f=!1;async function h(r,n,s,o){let e;try{e=await Promise.resolve().then(() => _interopRequireWildcard(require("@googlemaps/markerclusterer")))}catch(a){f||(f=!0,console.warn('[react-listing-engine] "@googlemaps/markerclusterer" is not installed -- rendering plain (unclustered) markers instead. Install "@googlemaps/markerclusterer" to enable clustering.',a));return}if(r.layers.get(n)!==s)return;let t=new e.MarkerClusterer({map:r.map,markers:s,algorithmOptions:{maxZoom:o.maxZoom},renderer:{render:a=>E(r.markerLib,a)}});r.clusterers.set(n,t)}function g(r,n){let s=r.clusterers.get(n);s&&(s.setMap(null),r.clusterers.delete(n))}function R(r){let n=r.getSouthWest(),s=r.getNorthEast();return{west:n.lng(),south:n.lat(),east:s.lng(),north:s.lat()}}function O(r,n,s){if(typeof ResizeObserver>"u")return;let o=!1,e=()=>{let{width:a,height:i}=r.getBoundingClientRect();a===0||i===0||(google.maps.event.trigger(n,"resize"),o||(o=!0,n.setCenter(s)))},t=new ResizeObserver(e);return t.observe(r),typeof requestAnimationFrame<"u"?requestAnimationFrame(e):queueMicrotask(e),t}function P(r){if(!r.apiKey)throw new Error("googleProvider: config.apiKey is required (no hardcoded fallback key is ever used).");let n=!1,s=()=>{n||(_jsapiloader.setOptions.call(void 0, {...r.loaderOptions,key:r.apiKey}),n=!0)};return{async mount(o,e){s();let t=await _jsapiloader.importLibrary.call(void 0, "maps"),a=await _jsapiloader.importLibrary.call(void 0, "marker"),i=_nullishCoalesce(e.center, () => ({lat:0,lng:0})),l=new t.Map(o,{center:i,zoom:_nullishCoalesce(e.zoom, () => (10)),mapId:_nullishCoalesce(r.mapId, () => (y))}),d=O(o,l,i);return{raw:{map:l,markerLib:a,layers:new Map,clusterers:new Map,boundsListeners:new Set,resizeObserver:d}}},renderLayer(o,e){let t=u(o),a=t.layers.get(e.id);a&&(p(a),t.layers.delete(e.id)),g(t,e.id);let i=e.markers.map(d=>{let c=new t.markerLib.AdvancedMarkerElement({map:t.map,position:d.position,content:v(d,t.markerLib),gmpClickable:!!e.onMarkerClick});if(e.onMarkerClick){let M=e.onMarkerClick;c.addListener("click",()=>M(d.id))}return c});t.layers.set(e.id,i),e.clustering&&h(t,e.id,i,e.clustering);let l=!1;return()=>{l||(l=!0,t.layers.get(e.id)===i&&(p(i),t.layers.delete(e.id),g(t,e.id)))}},onBoundsChange(o,e){let t=u(o),a=t.map.addListener("idle",()=>{let l=t.map.getBounds();l&&e(R(l))});t.boundsListeners.add(a);let i=!1;return()=>{i||(i=!0,a.remove(),t.boundsListeners.delete(a))}},fitBounds(o,e){u(o).map.fitBounds(e)},destroy(o){let e=u(o);_optionalChain([e, 'access', _ => _.resizeObserver, 'optionalAccess', _2 => _2.disconnect, 'call', _3 => _3()]);for(let t of e.boundsListeners)t.remove();e.boundsListeners.clear();for(let t of[...e.clusterers.keys()])g(e,t);for(let t of e.layers.values())p(t);e.layers.clear()}}}exports.googleProvider = P;
@@ -0,0 +1,39 @@
1
+ import { APIOptions } from '@googlemaps/js-api-loader';
2
+ import { M as MapProvider } from '../../map-provider.interface-BMnwW3ob.cjs';
3
+ import '../../entity-adapter.interface-BDgbSxhq.cjs';
4
+
5
+ interface GoogleMapsProviderConfig {
6
+ /** Google Maps JavaScript API key. Required -- there is no hardcoded fallback. */
7
+ apiKey: string;
8
+ /**
9
+ * Forwarded to every `google.maps.Map`. Required by Google for `AdvancedMarkerElement` to
10
+ * render at all -- a map created without a Map ID silently drops advanced markers (see
11
+ * https://developers.google.com/maps/documentation/javascript/advanced-markers/migration).
12
+ * Defaults to Google's documented zero-config dev Map ID (`DEFAULT_MAP_ID`,
13
+ * `'DEMO_MAP_ID'`) when omitted, so advanced markers work out of the box with no setup.
14
+ * `'DEMO_MAP_ID'` carries no custom styling and is not intended for production traffic --
15
+ * consumers should supply their own Cloud Console-configured Map ID for production use or
16
+ * custom map styling.
17
+ */
18
+ mapId?: string;
19
+ /**
20
+ * Extra `setOptions()` config (language, region, preloaded libraries, ...). `key` always
21
+ * comes from `apiKey` and cannot be overridden here.
22
+ */
23
+ loaderOptions?: Partial<Omit<APIOptions, 'key'>>;
24
+ }
25
+ /**
26
+ * `MapProvider` backed by the Google Maps JavaScript API.
27
+ *
28
+ * The installed `@googlemaps/js-api-loader` (v2) dropped the class-based `new Loader(...)` API
29
+ * in favor of module-level `setOptions()` + `importLibrary()` functions -- the `Loader` class
30
+ * that remains exported is a deprecated stub whose constructor throws synchronously (see that
31
+ * package's MIGRATION.md and `dist/index.js`). This implementation targets the functional API
32
+ * since that is the real, installed shape.
33
+ *
34
+ * Throws immediately if `config.apiKey` is falsy -- callers must always supply a real key via
35
+ * the factory; there is never a hardcoded fallback.
36
+ */
37
+ declare function googleProvider(config: GoogleMapsProviderConfig): MapProvider;
38
+
39
+ export { type GoogleMapsProviderConfig, googleProvider };
@@ -0,0 +1,39 @@
1
+ import { APIOptions } from '@googlemaps/js-api-loader';
2
+ import { M as MapProvider } from '../../map-provider.interface-BxexMT3X.js';
3
+ import '../../entity-adapter.interface-BDgbSxhq.js';
4
+
5
+ interface GoogleMapsProviderConfig {
6
+ /** Google Maps JavaScript API key. Required -- there is no hardcoded fallback. */
7
+ apiKey: string;
8
+ /**
9
+ * Forwarded to every `google.maps.Map`. Required by Google for `AdvancedMarkerElement` to
10
+ * render at all -- a map created without a Map ID silently drops advanced markers (see
11
+ * https://developers.google.com/maps/documentation/javascript/advanced-markers/migration).
12
+ * Defaults to Google's documented zero-config dev Map ID (`DEFAULT_MAP_ID`,
13
+ * `'DEMO_MAP_ID'`) when omitted, so advanced markers work out of the box with no setup.
14
+ * `'DEMO_MAP_ID'` carries no custom styling and is not intended for production traffic --
15
+ * consumers should supply their own Cloud Console-configured Map ID for production use or
16
+ * custom map styling.
17
+ */
18
+ mapId?: string;
19
+ /**
20
+ * Extra `setOptions()` config (language, region, preloaded libraries, ...). `key` always
21
+ * comes from `apiKey` and cannot be overridden here.
22
+ */
23
+ loaderOptions?: Partial<Omit<APIOptions, 'key'>>;
24
+ }
25
+ /**
26
+ * `MapProvider` backed by the Google Maps JavaScript API.
27
+ *
28
+ * The installed `@googlemaps/js-api-loader` (v2) dropped the class-based `new Loader(...)` API
29
+ * in favor of module-level `setOptions()` + `importLibrary()` functions -- the `Loader` class
30
+ * that remains exported is a deprecated stub whose constructor throws synchronously (see that
31
+ * package's MIGRATION.md and `dist/index.js`). This implementation targets the functional API
32
+ * since that is the real, installed shape.
33
+ *
34
+ * Throws immediately if `config.apiKey` is falsy -- callers must always supply a real key via
35
+ * the factory; there is never a hardcoded fallback.
36
+ */
37
+ declare function googleProvider(config: GoogleMapsProviderConfig): MapProvider;
38
+
39
+ export { type GoogleMapsProviderConfig, googleProvider };
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ import{importLibrary as m,setOptions as k}from"@googlemaps/js-api-loader";var y="DEMO_MAP_ID";function u(r){return r.raw}function v(r,n){if(r.element)return r.element;if(r.iconUrl)return new n.PinElement({glyphSrc:r.iconUrl})}function p(r){for(let n of r)n.map=null}var L="#b3261e",b=40,C=64,w=14;function E(r,n){let s=n.count,o=Math.min(C,b+Math.round(Math.log10(s+1)*w)),e=document.createElement("div");return e.textContent=String(s),e.style.display="flex",e.style.alignItems="center",e.style.justifyContent="center",e.style.width=`${o}px`,e.style.height=`${o}px`,e.style.borderRadius="50%",e.style.backgroundColor=L,e.style.border="2px solid #ffffff",e.style.boxShadow="0 1px 4px rgba(0, 0, 0, 0.4)",e.style.color="#ffffff",e.style.fontFamily="inherit",e.style.fontWeight="700",e.style.fontSize=o>=52?"15px":"13px",new r.AdvancedMarkerElement({position:n.position,content:e})}var f=!1;async function h(r,n,s,o){let e;try{e=await import("@googlemaps/markerclusterer")}catch(a){f||(f=!0,console.warn('[react-listing-engine] "@googlemaps/markerclusterer" is not installed -- rendering plain (unclustered) markers instead. Install "@googlemaps/markerclusterer" to enable clustering.',a));return}if(r.layers.get(n)!==s)return;let t=new e.MarkerClusterer({map:r.map,markers:s,algorithmOptions:{maxZoom:o.maxZoom},renderer:{render:a=>E(r.markerLib,a)}});r.clusterers.set(n,t)}function g(r,n){let s=r.clusterers.get(n);s&&(s.setMap(null),r.clusterers.delete(n))}function R(r){let n=r.getSouthWest(),s=r.getNorthEast();return{west:n.lng(),south:n.lat(),east:s.lng(),north:s.lat()}}function O(r,n,s){if(typeof ResizeObserver>"u")return;let o=!1,e=()=>{let{width:a,height:i}=r.getBoundingClientRect();a===0||i===0||(google.maps.event.trigger(n,"resize"),o||(o=!0,n.setCenter(s)))},t=new ResizeObserver(e);return t.observe(r),typeof requestAnimationFrame<"u"?requestAnimationFrame(e):queueMicrotask(e),t}function P(r){if(!r.apiKey)throw new Error("googleProvider: config.apiKey is required (no hardcoded fallback key is ever used).");let n=!1,s=()=>{n||(k({...r.loaderOptions,key:r.apiKey}),n=!0)};return{async mount(o,e){s();let t=await m("maps"),a=await m("marker"),i=e.center??{lat:0,lng:0},l=new t.Map(o,{center:i,zoom:e.zoom??10,mapId:r.mapId??y}),d=O(o,l,i);return{raw:{map:l,markerLib:a,layers:new Map,clusterers:new Map,boundsListeners:new Set,resizeObserver:d}}},renderLayer(o,e){let t=u(o),a=t.layers.get(e.id);a&&(p(a),t.layers.delete(e.id)),g(t,e.id);let i=e.markers.map(d=>{let c=new t.markerLib.AdvancedMarkerElement({map:t.map,position:d.position,content:v(d,t.markerLib),gmpClickable:!!e.onMarkerClick});if(e.onMarkerClick){let M=e.onMarkerClick;c.addListener("click",()=>M(d.id))}return c});t.layers.set(e.id,i),e.clustering&&h(t,e.id,i,e.clustering);let l=!1;return()=>{l||(l=!0,t.layers.get(e.id)===i&&(p(i),t.layers.delete(e.id),g(t,e.id)))}},onBoundsChange(o,e){let t=u(o),a=t.map.addListener("idle",()=>{let l=t.map.getBounds();l&&e(R(l))});t.boundsListeners.add(a);let i=!1;return()=>{i||(i=!0,a.remove(),t.boundsListeners.delete(a))}},fitBounds(o,e){u(o).map.fitBounds(e)},destroy(o){let e=u(o);e.resizeObserver?.disconnect();for(let t of e.boundsListeners)t.remove();e.boundsListeners.clear();for(let t of[...e.clusterers.keys()])g(e,t);for(let t of e.layers.values())p(t);e.layers.clear()}}}export{P as googleProvider};
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
2
+ var _chunkCHNUE46Kcjs = require('../../chunk-CHNUE46K.cjs');var _chunkLCQZIWBOcjs = require('../../chunk-LCQZIWBO.cjs');var _chunk2VUHLHHXcjs = require('../../chunk-2VUHLHHX.cjs');var Y=["grocery","shopping","restaurants","schools","hospitals","parks","gyms","cafes"];function _(e,t){let n=e.list;return{id:"businesses",adapter:{list:n?(r,o)=>n(r,o):async()=>({items:[],nextCursor:null}),getPoints:(r,o)=>e.search(r,o).then(s=>s.filter(i=>!_optionalChain([t, 'optionalAccess', _2 => _2.categories])||t.categories.includes(i.category))).then(s=>s.map(i=>({id:i.id,position:i.coordinates,entity:i})))},marker:{iconUrl:r=>_nullishCoalesce(_optionalChain([t, 'optionalAccess', _3 => _3.icons, 'optionalAccess', _4 => _4[r.category]]), () => ("")),onClick:_optionalChain([t, 'optionalAccess', _5 => _5.onClick])},clustering:{maxZoom:15},visible:()=>!0}}var _jsxruntime = require('react/jsx-runtime');function b({value:e,onChange:t}){return _jsxruntime.jsx.call(void 0, "input",{type:"search",value:e,onChange:r=>{t(r.target.value)},"aria-label":"Keyword",placeholder:"Search keyword",className:"h-9 w-full rounded-md border border-border bg-background px-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"})}var _react = require('react');var k=["house","apartment","condo","townhouse","land"],N={house:"House",apartment:"Apartment",condo:"Condo",townhouse:"Townhouse",land:"Land"};function F({value:e,onChange:t}){let[n,r]=_react.useState.call(void 0, !1),o=i=>l=>{t(l.target.checked?[...e,i]:e.filter(c=>c!==i))};return _jsxruntime.jsxs.call(void 0, "details",{open:n,className:"group relative",children:[_jsxruntime.jsxs.call(void 0, "summary",{onClick:i=>{i.preventDefault(),r(l=>!l)},"aria-expanded":n,className:"flex h-9 w-fit cursor-pointer list-none items-center gap-1.5 rounded-md border border-border bg-background px-3 text-sm text-foreground motion-safe:transition-colors hover:bg-foreground/[0.06] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&::-webkit-details-marker]:hidden",children:["Property type",e.length>0&&_jsxruntime.jsx.call(void 0, "span",{className:"inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-xs font-semibold tabular-nums text-primary-foreground",children:e.length}),_jsxruntime.jsx.call(void 0, "svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round",className:"size-3.5 shrink-0 text-muted-foreground motion-safe:transition-transform group-open:rotate-180",children:_jsxruntime.jsx.call(void 0, "path",{d:"m6 9 6 6 6-6"})})]}),_jsxruntime.jsxs.call(void 0, "fieldset",{className:"absolute z-10 mt-1.5 flex w-48 flex-col gap-1.5 rounded-md border border-border bg-popover p-3 text-popover-foreground shadow-md",children:[_jsxruntime.jsx.call(void 0, "legend",{className:"sr-only",children:"Property type"}),k.map(i=>_jsxruntime.jsxs.call(void 0, "label",{className:"flex items-center gap-2 text-sm",children:[_jsxruntime.jsx.call(void 0, "input",{type:"checkbox",checked:e.includes(i),onChange:o(i),className:"size-4 rounded border-border accent-primary"}),N[i]]},i))]})]})}function R(e){if(e==="")return;let t=Number(e);return Number.isNaN(t)?void 0:t}function y({value:e,onChange:t}){let n=s=>{t({...e,min:R(s.target.value)})},r=s=>{t({...e,max:R(s.target.value)})},o="h-9 w-full rounded-md border border-border bg-background px-2 text-sm tabular-nums text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";return _jsxruntime.jsxs.call(void 0, "div",{className:"flex items-center gap-2",children:[_jsxruntime.jsx.call(void 0, "input",{type:"number",inputMode:"numeric","aria-label":"Minimum",placeholder:"Min",value:_nullishCoalesce(e.min, () => ("")),onChange:n,className:o}),_jsxruntime.jsx.call(void 0, "span",{"aria-hidden":"true",className:"text-muted-foreground",children:"-"}),_jsxruntime.jsx.call(void 0, "input",{type:"number",inputMode:"numeric","aria-label":"Maximum",placeholder:"Max",value:_nullishCoalesce(e.max, () => ("")),onChange:r,className:o})]})}function f(e){return new Intl.NumberFormat("en-US",{style:"currency",currency:"USD",maximumFractionDigits:0}).format(e)}function v(e){return e.length?`${e[0].toUpperCase()}${e.slice(1)}`:e}function J({item:e,selected:t,onSelect:n}){let r=e,o=_chunkLCQZIWBOcjs.a.call(void 0, "flex w-full flex-col overflow-hidden rounded-[10px] border border-border bg-card text-left text-card-foreground","motion-safe:transition-shadow",n&&"hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",t&&"ring-2 ring-ring"),s=_jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment,{children:[_optionalChain([r, 'optionalAccess', _6 => _6.imageUrl])?_jsxruntime.jsx.call(void 0, "img",{src:r.imageUrl,alt:_nullishCoalesce(r.title, () => ("")),className:"aspect-[4/3] w-full rounded-t-[10px] object-cover"}):_jsxruntime.jsx.call(void 0, "div",{className:"aspect-[4/3] w-full rounded-t-[10px] bg-muted","aria-hidden":"true"}),_jsxruntime.jsxs.call(void 0, "div",{className:"flex flex-1 flex-col gap-1 p-3",children:[_optionalChain([r, 'optionalAccess', _7 => _7.title])&&_jsxruntime.jsx.call(void 0, "span",{className:"text-base font-semibold",children:r.title}),_optionalChain([r, 'optionalAccess', _8 => _8.address])&&_jsxruntime.jsx.call(void 0, "span",{className:"text-sm text-muted-foreground",children:r.address}),_optionalChain([r, 'optionalAccess', _9 => _9.propertyType])!=null&&r.bedrooms!=null&&r.bathrooms!=null&&_jsxruntime.jsxs.call(void 0, "span",{className:"text-sm text-muted-foreground",children:[v(r.propertyType)," \xB7 ",r.bedrooms," bd \xB7 ",r.bathrooms," ba"]}),_optionalChain([r, 'optionalAccess', _10 => _10.price])!=null&&_jsxruntime.jsxs.call(void 0, "span",{className:"mt-1 font-bold tabular-nums",children:[f(r.price)," ",_jsxruntime.jsx.call(void 0, "span",{className:"text-sm font-normal text-muted-foreground",children:"/mo"})]})]})]});return n?_jsxruntime.jsx.call(void 0, "button",{type:"button",onClick:n,"aria-pressed":_nullishCoalesce(t, () => (!1)),className:o,children:s}):_jsxruntime.jsx.call(void 0, "article",{className:o,children:s})}function ee({point:e}){let t=e.entity;return _jsxruntime.jsxs.call(void 0, "span",{className:"relative inline-flex items-center whitespace-nowrap rounded-full bg-primary px-2.5 py-1 text-xs font-bold tabular-nums text-primary-foreground shadow",children:[_optionalChain([t, 'optionalAccess', _11 => _11.price])!=null?f(t.price):"",_jsxruntime.jsx.call(void 0, "span",{"aria-hidden":"true",className:"absolute -bottom-[3px] left-1/2 h-2 w-2 -translate-x-1/2 rotate-45 rounded-br-[2px] bg-primary"})]})}function S(e){let t=document.createElement("div");t.className="relative inline-flex items-center whitespace-nowrap rounded-full bg-primary px-2.5 py-1 text-xs font-bold text-primary-foreground shadow",t.textContent=f(e.price),t.style.position="relative",t.style.display="inline-flex",t.style.alignItems="center",t.style.whiteSpace="nowrap",t.style.borderRadius="9999px",t.style.backgroundColor="var(--primary, #16a34a)",t.style.color="var(--primary-foreground, #ffffff)",t.style.padding="4px 10px",t.style.fontSize="12px",t.style.fontWeight="700",t.style.fontFamily="inherit",t.style.boxShadow="0 1px 2px rgba(0, 0, 0, 0.3)";let n=document.createElement("span");return n.className="absolute -bottom-[3px] left-1/2 h-2 w-2 -translate-x-1/2 rotate-45 rounded-br-[2px] bg-primary",n.style.position="absolute",n.style.left="50%",n.style.bottom="-3px",n.style.width="8px",n.style.height="8px",n.style.borderRadius="0 0 2px 0",n.style.backgroundColor="var(--primary, #16a34a)",n.style.transform="translateX(-50%) rotate(45deg)",t.appendChild(n),t}function ie(e,t){let n=e.getById;return{id:"properties",adapter:{list:(r,o)=>e.list(r,o),getPoints:(r,o)=>e.search(r,o).then(s=>s.map(i=>({id:i.id,position:i.coordinates,entity:i}))),getById:n?r=>n(r):void 0},marker:{iconUrl:_optionalChain([t, 'optionalAccess', _12 => _12.iconUrl]),element:_nullishCoalesce(_optionalChain([t, 'optionalAccess', _13 => _13.element]), () => (S)),onClick:_optionalChain([t, 'optionalAccess', _14 => _14.onClick])},clustering:{maxZoom:14}}}var D={key:"price",order:10,label:"Price",render:y,toParams:e=>({minPrice:e.min,maxPrice:e.max}),fromParams:e=>({min:e.minPrice,max:e.maxPrice}),isActive:e=>e.minPrice!=null||e.maxPrice!=null},I={key:"beds",order:20,label:"Bedrooms",render:y,toParams:e=>({minBeds:e.min,maxBeds:e.max}),fromParams:e=>({min:e.minBeds,max:e.maxBeds}),isActive:e=>e.minBeds!=null||e.maxBeds!=null},M={key:"baths",order:30,label:"Bathrooms",render:y,toParams:e=>({minBaths:e.min,maxBaths:e.max}),fromParams:e=>({min:e.minBaths,max:e.maxBaths}),isActive:e=>e.minBaths!=null||e.maxBaths!=null},L={key:"propertyType",order:40,label:"Property type",render:F,toParams:e=>({propertyTypes:e.length?e:void 0}),fromParams:e=>_nullishCoalesce(e.propertyTypes, () => ([])),isActive:e=>(_nullishCoalesce(_optionalChain([e, 'access', _15 => _15.propertyTypes, 'optionalAccess', _16 => _16.length]), () => (0)))>0},O={key:"keyword",order:50,label:"Keyword",render:b,toParams:e=>({keyword:e||void 0}),fromParams:e=>_nullishCoalesce(e.keyword, () => ("")),isActive:e=>!!e.keyword},U= exports.rentalFilters =[D,I,M,L,O];function pe(){return e=>{U.forEach(t=>e.add(t))}}var A=["house","apartment","condo","townhouse","land"];function V(e){return A.includes(e)}function p(e){if(e===void 0||e==="")return;let t=Number(e);return Number.isNaN(t)?void 0:t}function H(e){let t={};return e.minPrice!=null&&(t.minPrice=String(e.minPrice)),e.maxPrice!=null&&(t.maxPrice=String(e.maxPrice)),e.minBeds!=null&&(t.minBeds=String(e.minBeds)),e.maxBeds!=null&&(t.maxBeds=String(e.maxBeds)),e.minBaths!=null&&(t.minBaths=String(e.minBaths)),e.maxBaths!=null&&(t.maxBaths=String(e.maxBaths)),_optionalChain([e, 'access', _17 => _17.propertyTypes, 'optionalAccess', _18 => _18.length])&&(t.type=e.propertyTypes.join(",")),e.keyword&&(t.q=e.keyword),t}function Q(e){let t={},n=p(e.minPrice);n!==void 0&&(t.minPrice=n);let r=p(e.maxPrice);r!==void 0&&(t.maxPrice=r);let o=p(e.minBeds);o!==void 0&&(t.minBeds=o);let s=p(e.maxBeds);s!==void 0&&(t.maxBeds=s);let i=p(e.minBaths);i!==void 0&&(t.minBaths=i);let l=p(e.maxBaths);l!==void 0&&(t.maxBaths=l);let c=_optionalChain([e, 'access', _19 => _19.type, 'optionalAccess', _20 => _20.split, 'call', _21 => _21(","), 'access', _22 => _22.map, 'call', _23 => _23(B=>B.trim()), 'access', _24 => _24.filter, 'call', _25 => _25(V)]);return _optionalChain([c, 'optionalAccess', _26 => _26.length])&&(t.propertyTypes=c),e.q&&(t.keyword=e.q),t}function ue(e={}){let t=_nullishCoalesce(e.history, () => (new (0, _chunkCHNUE46Kcjs.a)({mode:e.mode})));return new (0, _chunk2VUHLHHXcjs.a)({history:t,toQuery:H,toFilters:Q,hydrateOnStart:e.hydrateOnStart})}exports.KNOWN_BUSINESS_CATEGORIES = Y; exports.KeywordFilterControl = b; exports.PropertyCard = J; exports.PropertyMarker = ee; exports.PropertyTypeFilterControl = F; exports.RangeFilterControl = y; exports.defaultPriceMarkerElement = S; exports.formatRentalPrice = f; exports.nearbyBusinessesDataset = _; exports.propertiesDataset = ie; exports.rentalFilters = U; exports.rentalFiltersFromQuery = Q; exports.rentalFiltersToQuery = H; exports.rentalUrlSync = ue; exports.withRentalFilters = pe;