react-listing-engine 0.6.5 → 0.6.7

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.
package/dist/index.d.ts CHANGED
@@ -1,12 +1,9 @@
1
1
  import { P as Page, B as Bounds, E as EntityId, M as MapPoint, U as Unsubscribe, a as MapProvider, Q as QueryParams, L as LatLng } from './map-provider.interface-DT-v1plm.js';
2
2
  export { b as EntityAdapter, c as MapHandle, d as MapInitOptions, e as PageRequest, R as RenderedLayer } from './map-provider.interface-DT-v1plm.js';
3
- import { P as PaginationMode, D as DatasetDefinition, I as IListingConfigOptions, F as FilterRegistry, a as IListingToolbarProps } from './components-provider-cWVWEDXC.js';
4
- export { C as ClusterOptions, b as FilterControlProps, c as FilterDefinition, d as IListingCardProps, e as IListingComponents, f as IListingEmptyProps, g as IListingFilterPanelProps, h as IListingLoadingProps, i as IListingMarkerProps, j as IListingPopupProps, k as IListingResultHeaderProps, l as IListingSearchProps, m as IListingSidebarProps, L as ListingComponentsProvider, M as MarkerRenderer, u as useListingComponents } from './components-provider-cWVWEDXC.js';
5
- import { H as HistoryPort, U as UrlSyncController } from './url-sync.controller-DK5W_0OZ.js';
6
- export { a as UrlSyncEngine, b as UrlSyncOptions } from './url-sync.controller-DK5W_0OZ.js';
3
+ import { P as PaginationMode, D as DatasetDefinition, I as IListingConfigOptions, F as FilterRegistry, a as IListingToolbarProps } from './listing-app-IG1awue3.js';
4
+ export { C as ClusterOptions, b as FilterControlProps, c as FilterDefinition, d as IListingCardProps, e as IListingComponents, f as IListingEmptyProps, g as IListingFilterPanelProps, h as IListingLoadingProps, i as IListingMarkerProps, j as IListingPopupProps, k as IListingResultHeaderProps, l as IListingSearchProps, m as IListingSidebarProps, L as ListingApp, n as ListingAppProps, o as ListingComponentsProvider, M as MarkerRenderer, u as useListingComponents } from './listing-app-IG1awue3.js';
7
5
  import * as react from 'react';
8
6
  import { ReactNode } from 'react';
9
- export { L as ListingApp, a as ListingAppProps } from './listing-app-BSJKGh7k.js';
10
7
 
11
8
  declare enum ListingEventType {
12
9
  FiltersChanged = "FiltersChanged",
@@ -213,6 +210,18 @@ declare class ListingEngine<TEntity, TFilters> {
213
210
  private clearDebounce;
214
211
  }
215
212
 
213
+ /**
214
+ * DOM-agnostic abstraction over "the URL's query string" — or, in tests/SSR,
215
+ * an in-memory stand-in (`MemoryHistoryPort`). `UrlSyncController` talks to
216
+ * this interface only, never to `window.location`/`history` directly, so it
217
+ * stays framework- and environment-free like the rest of `src/core`.
218
+ */
219
+ interface HistoryPort {
220
+ getQuery(): QueryParams;
221
+ setQuery(params: QueryParams): void;
222
+ subscribe(cb: () => void): () => void;
223
+ }
224
+
216
225
  /**
217
226
  * In-memory `HistoryPort` — holds `QueryParams` in a plain field instead of
218
227
  * the browser URL. Used by tests (fast, no DOM/jsdom `history` needed) and by
@@ -228,6 +237,79 @@ declare class MemoryHistoryPort implements HistoryPort {
228
237
  private notify;
229
238
  }
230
239
 
240
+ interface UrlSyncEngine<TFilters> {
241
+ subscribe(cb: () => void): () => void;
242
+ readonly state: {
243
+ readonly filters: TFilters;
244
+ };
245
+ applyFilters(patch: Partial<TFilters>): Promise<void> | void;
246
+ }
247
+ interface UrlSyncOptions<TFilters> {
248
+ history: HistoryPort;
249
+ toQuery(filters: TFilters): QueryParams;
250
+ toFilters(query: QueryParams): Partial<TFilters>;
251
+ hydrateOnStart?: boolean;
252
+ }
253
+ /**
254
+ * Bidirectional, DOM-agnostic sync between an engine's `filters` and a
255
+ * `HistoryPort`'s query params. Framework-free: talks to `UrlSyncEngine`
256
+ * (a structural subset `ListingEngine` satisfies) and `HistoryPort`, nothing
257
+ * else — no `window`, no React.
258
+ *
259
+ * OPTIONAL, not the primary path: `styled/listing-app.tsx`'s turnkey
260
+ * `ListingApp` (the package's main-entry default) does NOT wire this up —
261
+ * it uses an event-based URL API instead (`initialFilters` in,
262
+ * `onFiltersChange` out; see that file's doc comment), so the library never
263
+ * touches `window.history` by default. This class remains exported for
264
+ * consumers who explicitly want the library to own history writes/reads
265
+ * itself, by wiring it directly against a hand-built engine.
266
+ *
267
+ * Echo-loop guard: both subscriptions below are driven by the SAME
268
+ * `isSyncing` flag. `ListingEngine.applyFilters()` and `MemoryHistoryPort`
269
+ * (and any real browser HistoryPort) both notify their subscribers
270
+ * SYNCHRONOUSLY as part of the write (`store.setFilters()` -> `notify()`
271
+ * happens before any debounce timer, and `MemoryHistoryPort.setQuery()`
272
+ * notifies before returning) — so when this controller initiates a write on
273
+ * one side, the reciprocal subscription on the other side fires within the
274
+ * very same call stack, before `isSyncing` is reset. Wrapping each
275
+ * controller-initiated write in `isSyncing = true; ...; isSyncing = false`
276
+ * (via try/finally, so a throwing `toQuery`/`toFilters`/notify can't leave it
277
+ * stuck) means that reciprocal callback observes `isSyncing === true` and
278
+ * short-circuits instead of writing back — one hop each direction, never a
279
+ * cascade. `hydrateOnStart`'s initial engine write is wrapped the same way,
280
+ * since it must not immediately echo the just-hydrated filters back out to
281
+ * history as a redundant `setQuery`.
282
+ *
283
+ * `isSyncing` only covers that SYNCHRONOUS window, though — the real
284
+ * `ListingEngine.applyFilters()` notifies AGAIN, asynchronously: after
285
+ * `await adapter.list(...)` resolves, `store.setResults()` and
286
+ * `store.setLoading(false)` each call `notify()`, well after `isSyncing` has
287
+ * already been reset to `false` by the `withSyncGuard` that kicked the query
288
+ * off. Left unguarded, that async tail would fire the engine subscription
289
+ * below and echo an identical (but spurious) `history.setQuery(...)` on
290
+ * every completed query. Rather than widening `isSyncing` to cover the whole
291
+ * async query (which would also swallow legitimate concurrent history
292
+ * changes that land mid-query), `syncEngineToHistory` is idempotent BY
293
+ * VALUE instead: it computes the target query and skips the write entirely
294
+ * when it already matches `history.getQuery()` — timing-independent, so it
295
+ * suppresses the async-tail echo without touching `isSyncing` at all.
296
+ */
297
+ declare class UrlSyncController<TFilters> {
298
+ private readonly history;
299
+ private readonly toQueryFn;
300
+ private readonly toFiltersFn;
301
+ private readonly hydrateOnStart;
302
+ private isSyncing;
303
+ private unsubscribeEngine;
304
+ private unsubscribeHistory;
305
+ constructor(opts: UrlSyncOptions<TFilters>);
306
+ start(engine: UrlSyncEngine<TFilters>): void;
307
+ stop(): void;
308
+ private syncEngineToHistory;
309
+ private applyFiltersSafely;
310
+ private withSyncGuard;
311
+ }
312
+
231
313
  /**
232
314
  * Aggregated provider props — the composition entry point of
233
315
  * react-listing-engine, mirroring `react-wizard-engine`'s
@@ -296,8 +378,8 @@ interface IListingFiltersProps {
296
378
  * styling hook -- this component stays structure-only, so it never bakes
297
379
  * in its own layout beyond the `space-y-5` fallback below. Pass a
298
380
  * horizontal row (e.g. `"flex flex-wrap items-end gap-3"`) to flow filter
299
- * groups inline instead of stacking them, as `/shadcn`'s `ListingLayout`
300
- * does for its top filter bar.
381
+ * groups inline instead of stacking them, as the `/styled` adapter's
382
+ * `StyledListingLayout` does for its top filter bar.
301
383
  */
302
384
  className?: string;
303
385
  /** ClassName applied to each individual filter group's wrapper `<div>` (including the string-placeholder case). */
@@ -322,7 +404,7 @@ interface IListingFiltersProps {
322
404
  * `"text"`/`"range"`/`"toggle"`): there is no shared named-control registry
323
405
  * yet, so this renders an inert `<div data-filter={def.key} />` placeholder.
324
406
  * Wiring a real named-control registry (so `"range"` etc. resolve to an
325
- * actual control, likely in the `/shadcn` adapter) is a documented future
407
+ * actual control) is a documented future
326
408
  * enhancement, not attempted in this task.
327
409
  *
328
410
  * Each filter group is wrapped in its own `<div>`: when `def.label` is set,
@@ -488,8 +570,8 @@ declare function ListingMap(props: IListingMapProps): react.JSX.Element;
488
570
  *
489
571
  * Renders a plain `<button>` rather than an injected component: `useListingComponents()`
490
572
  * (`IListingComponents`) has no `Button` slot yet, so there is nothing to
491
- * delegate to here. Wiring this up to a real Button slot (and to the shadcn
492
- * adapter's styled Button) is a documented future enhancement once that slot
573
+ * delegate to here. Wiring this up to a real Button slot (and a styled Button
574
+ * in the `/styled` adapter) is a documented future enhancement once that slot
493
575
  * exists.
494
576
  */
495
577
  declare function ListingPagination(): react.JSX.Element | null;
@@ -676,4 +758,4 @@ interface IListingProviderComponentProps<TFilters> extends IListingProviderProps
676
758
  */
677
759
  declare function ListingProvider<TEntity, TFilters>(props: IListingProviderComponentProps<TFilters>): react.JSX.Element | null;
678
760
 
679
- export { Bounds, BrowserHistoryPort, type BrowserHistoryPortOptions, DatasetDefinition, DatasetRegistry, EntityId, FilterRegistry, HistoryPort, IListingConfigOptions, type IListingFiltersProps, type IListingMapProps, type IListingProviderProps, IListingToolbarProps, LatLng, ListingConfig, ListingEngine, ListingEngineContext, type ListingEngineOptions, type ListingEvent, ListingEventType, ListingFilters, ListingList, ListingMap, ListingPagination, ListingProvider, type ListingProviderMod, ListingResultHeader, type ListingState, ListingStore, type ListingStoreInit, ListingToolbar, MapPoint, MapProvider, MemoryHistoryPort, Page, PaginationMode, QueryParams, TypedEmitter, Unsubscribe, UrlSyncController, composeListingProviders, listingDefaultConfig, useListing, useListingEvent, useListingFilters, useListingLayer, useListingMap, useListingResults, useListingState, withConfig, withDataset, withFilters, withInitialFilters, withMap, withPrimaryDataset, withUrlSync };
761
+ export { Bounds, BrowserHistoryPort, type BrowserHistoryPortOptions, DatasetDefinition, DatasetRegistry, EntityId, FilterRegistry, type HistoryPort, IListingConfigOptions, type IListingFiltersProps, type IListingMapProps, type IListingProviderProps, IListingToolbarProps, LatLng, ListingConfig, ListingEngine, ListingEngineContext, type ListingEngineOptions, type ListingEvent, ListingEventType, ListingFilters, ListingList, ListingMap, ListingPagination, ListingProvider, type ListingProviderMod, ListingResultHeader, type ListingState, ListingStore, type ListingStoreInit, ListingToolbar, MapPoint, MapProvider, MemoryHistoryPort, Page, PaginationMode, QueryParams, TypedEmitter, Unsubscribe, UrlSyncController, type UrlSyncEngine, type UrlSyncOptions, composeListingProviders, listingDefaultConfig, useListing, useListingEvent, useListingFilters, useListingLayer, useListingMap, useListingResults, useListingState, withConfig, withDataset, withFilters, withInitialFilters, withMap, withPrimaryDataset, withUrlSync };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  "use client";
2
- import{a as S}from"./chunk-35KYEBAC.js";import{a as K,p as f}from"./chunk-CGT3RHJ7.js";import{A as J,B as N,a as P,b as L,c as x,d as b,e as v,f as Q,g as C,h as k,i as q,j as B,k as I,l as H,m as R,n as $,o as O,p as A,q as p,r as F,s as n,t as a,u as T,v as U,w as j,x as z,y as D,z as G}from"./chunk-R3FX2V3N.js";var y=(o=>(o.Paged="paged",o.Infinite="infinite",o))(y||{});var h=(r=>(r.FiltersChanged="FiltersChanged",r.ResultsLoaded="ResultsLoaded",r.PointClicked="PointClicked",r.BoundsChanged="BoundsChanged",r.LayerToggled="LayerToggled",r))(h||{});var d=class{query;listeners=new Set;constructor(e={}){this.query={...e}}getQuery(){return{...this.query}}setQuery(e){this.query={...e},this.notify()}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}notify(){for(let e of[...this.listeners])e()}};var c=class{mode;constructor(e={}){this.mode=e.mode??"replace"}getQuery(){if(typeof window>"u")return{};let e=new URLSearchParams(window.location.search),o={};for(let[i,s]of e)s!==""&&(o[i]=s);return o}setQuery(e){if(typeof window>"u")return;let o=new URLSearchParams;for(let[m,u]of Object.entries(e))u===void 0||u===""||o.set(m,u);let i=o.toString(),s=i?`?${i}`:"";if(s===window.location.search)return;let r=`${window.location.pathname}${s}${window.location.hash}`;this.mode==="push"?window.history.pushState(window.history.state,"",r):window.history.replaceState(window.history.state,"",r)}subscribe(e){return typeof window>"u"?()=>{}:(window.addEventListener("popstate",e),()=>{window.removeEventListener("popstate",e)})}};import{jsx as w}from"react/jsx-runtime";function _({children:t}){let{Toolbar:e}=p();return w(e,{children:t})}import{useCallback as l}from"react";function re(){let t=n(),e=a(),o=l(s=>t.loadPoints(s),[t]),i=l((s,r)=>t.selectPoint(s,r),[t]);return{bounds:e.bounds,points:e.points,loadPoints:o,selectPoint:i}}import{useCallback as g}from"react";function ue(t){let e=n(),o=a(),i=g(()=>e.toggleLayer(t),[e,t]);return{visible:o.layers[t]??!0,points:o.points[t]??[],toggle:i}}export{c as BrowserHistoryPort,x as DatasetRegistry,L as FilterRegistry,f as ListingApp,A as ListingComponentsProvider,Q as ListingConfig,C as ListingEngine,F as ListingEngineContext,h as ListingEventType,U as ListingFilters,z as ListingList,D as ListingMap,G as ListingPagination,N as ListingProvider,J as ListingResultHeader,P as ListingStore,_ as ListingToolbar,d as MemoryHistoryPort,y as PaginationMode,b as TypedEmitter,S as UrlSyncController,k as composeListingProviders,v as listingDefaultConfig,n as useListing,p as useListingComponents,K as useListingEvent,T as useListingFilters,ue as useListingLayer,re as useListingMap,j as useListingResults,a as useListingState,q as withConfig,I as withDataset,H as withFilters,$ as withInitialFilters,B as withMap,O as withPrimaryDataset,R as withUrlSync};
2
+ import{A as J,B as K,C as N,R as f,a as w,b as F,c as S,d as Q,e as x,f as T,g as L,h as H,i as O,j as k,k as q,l as E,m as C,n as U,o as B,p as I,q as l,r as G,s as n,t as a,u as R,v as $,w as j,x as A,y as z,z as D}from"./chunk-GSWGCJCO.js";var m=(e=>(e.Paged="paged",e.Infinite="infinite",e))(m||{});var g=(s=>(s.FiltersChanged="FiltersChanged",s.ResultsLoaded="ResultsLoaded",s.PointClicked="PointClicked",s.BoundsChanged="BoundsChanged",s.LayerToggled="LayerToggled",s))(g||{});var y=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()}};function P(r,t){let e=new Set([...Object.keys(r),...Object.keys(t)]);for(let i of e)if(r[i]!==t[i])return!1;return!0}var c=class{history;toQueryFn;toFiltersFn;hydrateOnStart;isSyncing=!1;unsubscribeEngine=null;unsubscribeHistory=null;constructor(t){this.history=t.history,this.toQueryFn=t.toQuery,this.toFiltersFn=t.toFilters,this.hydrateOnStart=t.hydrateOnStart??!0}start(t){this.stop(),this.unsubscribeEngine=t.subscribe(()=>{this.isSyncing||this.syncEngineToHistory(t)}),this.unsubscribeHistory=this.history.subscribe(()=>{this.isSyncing||this.withSyncGuard(()=>{this.applyFiltersSafely(t,this.toFiltersFn(this.history.getQuery()))})}),this.hydrateOnStart&&this.withSyncGuard(()=>{this.applyFiltersSafely(t,this.toFiltersFn(this.history.getQuery()))})}stop(){this.unsubscribeEngine?.(),this.unsubscribeEngine=null,this.unsubscribeHistory?.(),this.unsubscribeHistory=null}syncEngineToHistory(t){let e=this.toQueryFn(t.state.filters);P(e,this.history.getQuery())||this.withSyncGuard(()=>this.history.setQuery(e))}applyFiltersSafely(t,e){Promise.resolve(t.applyFilters(e)).catch(()=>{})}withSyncGuard(t){this.isSyncing=!0;try{t()}finally{this.isSyncing=!1}}};var p=class{mode;constructor(t={}){this.mode=t.mode??"replace"}getQuery(){if(typeof window>"u")return{};let t=new URLSearchParams(window.location.search),e={};for(let[i,o]of t)o!==""&&(e[i]=o);return e}setQuery(t){if(typeof window>"u")return;let e=new URLSearchParams;for(let[h,u]of Object.entries(t))u===void 0||u===""||e.set(h,u);let i=e.toString(),o=i?`?${i}`:"";if(o===window.location.search)return;let s=`${window.location.pathname}${o}${window.location.hash}`;this.mode==="push"?window.history.pushState(window.history.state,"",s):window.history.replaceState(window.history.state,"",s)}subscribe(t){return typeof window>"u"?()=>{}:(window.addEventListener("popstate",t),()=>{window.removeEventListener("popstate",t)})}};import{jsx as b}from"react/jsx-runtime";function M({children:r}){let{Toolbar:t}=l();return b(t,{children:r})}import{useCallback as d}from"react";function ot(){let r=n(),t=a(),e=d(o=>r.loadPoints(o),[r]),i=d((o,s)=>r.selectPoint(o,s),[r]);return{bounds:t.bounds,points:t.points,loadPoints:e,selectPoint:i}}import{useCallback as v}from"react";function yt(r){let t=n(),e=a(),i=v(()=>t.toggleLayer(r),[t,r]);return{visible:e.layers[r]??!0,points:e.points[r]??[],toggle:i}}export{p as BrowserHistoryPort,S as DatasetRegistry,F as FilterRegistry,f as ListingApp,I as ListingComponentsProvider,T as ListingConfig,L as ListingEngine,G as ListingEngineContext,g as ListingEventType,$ as ListingFilters,A as ListingList,z as ListingMap,D as ListingPagination,N as ListingProvider,J as ListingResultHeader,w as ListingStore,M as ListingToolbar,y as MemoryHistoryPort,m as PaginationMode,Q as TypedEmitter,c as UrlSyncController,H as composeListingProviders,x as listingDefaultConfig,n as useListing,l as useListingComponents,K as useListingEvent,R as useListingFilters,yt as useListingLayer,ot as useListingMap,j as useListingResults,a as useListingState,O as withConfig,q as withDataset,E as withFilters,U as withInitialFilters,k as withMap,B as withPrimaryDataset,C as withUrlSync};
@@ -0,0 +1,347 @@
1
+ import * as react from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
+ import { b as EntityAdapter, M as MapPoint, L as LatLng, a as MapProvider } from './map-provider.interface-DT-v1plm.cjs';
4
+
5
+ interface FilterControlProps<TValue> {
6
+ value: TValue;
7
+ onChange(next: TValue): void;
8
+ }
9
+ interface FilterDefinition<TFilters, TValue = unknown> {
10
+ key: string;
11
+ order: number;
12
+ render: string | ComponentType<FilterControlProps<TValue>>;
13
+ toParams(value: TValue): Partial<TFilters>;
14
+ fromParams(filters: TFilters): TValue;
15
+ isActive?(filters: TFilters): boolean;
16
+ /** Optional human-readable label rendered above the control by `ListingFilters`. Omit for an unlabeled filter (backward compatible). */
17
+ label?: string;
18
+ }
19
+
20
+ interface ClusterOptions {
21
+ maxZoom?: number;
22
+ radius?: number;
23
+ }
24
+ interface MarkerRenderer<TEntity> {
25
+ iconUrl?(entity: TEntity): string;
26
+ element?(entity: TEntity): HTMLElement;
27
+ onClick?(entity: TEntity): void;
28
+ }
29
+ interface DatasetDefinition<TEntity, TFilters> {
30
+ id: string;
31
+ adapter: EntityAdapter<TEntity, TFilters>;
32
+ marker: MarkerRenderer<TEntity>;
33
+ clustering?: ClusterOptions | false;
34
+ visible?: () => boolean;
35
+ }
36
+
37
+ declare enum PaginationMode {
38
+ Paged = "paged",
39
+ Infinite = "infinite"
40
+ }
41
+
42
+ interface IListingConfigOptions {
43
+ pagination: PaginationMode;
44
+ pageSize: number;
45
+ debounceMs: number;
46
+ }
47
+
48
+ /**
49
+ * Framework-free registry for `FilterDefinition`s: programmatic add / remove /
50
+ * reorder / replace, plus folding raw control values into `TFilters`
51
+ * (`toFilters`), reporting which registered filters currently affect an
52
+ * applied `TFilters` (`activeKeys`), and building the reset-everything patch
53
+ * (`clearedParams`).
54
+ *
55
+ * Defs are stored by VALUE (shallow-cloned on `add`/`replace`), never by the
56
+ * caller's original reference — `reorder()` rewrites `order` in place on the
57
+ * registry's own copy. This is deliberate: the same `FilterDefinition` object
58
+ * can legitimately be registered in more than one `FilterRegistry` instance
59
+ * (e.g. two listing pages sharing a base filter set), and without cloning,
60
+ * one registry's `reorder()` would silently mutate `.order` on the shared
61
+ * object and bleed into every other registry (and the caller) holding it.
62
+ */
63
+ declare class FilterRegistry<TFilters> {
64
+ private readonly defs;
65
+ add<TValue = unknown>(def: FilterDefinition<TFilters, TValue>): this;
66
+ remove(key: string): this;
67
+ replace<TValue = unknown>(key: string, def: FilterDefinition<TFilters, TValue>): this;
68
+ reorder(keys: string[]): this;
69
+ list(): FilterDefinition<TFilters>[];
70
+ has(key: string): boolean;
71
+ toFilters(values: Record<string, unknown>): TFilters;
72
+ activeKeys(filters: TFilters): string[];
73
+ clearedParams(): Partial<TFilters>;
74
+ }
75
+
76
+ interface IListingCardProps {
77
+ item: unknown;
78
+ selected?: boolean;
79
+ onSelect?: () => void;
80
+ }
81
+ interface IListingMarkerProps {
82
+ point: MapPoint<unknown>;
83
+ }
84
+ interface IListingPopupProps {
85
+ entity: unknown;
86
+ onClose?: () => void;
87
+ }
88
+ interface IListingSidebarProps {
89
+ children?: ReactNode;
90
+ }
91
+ interface IListingFilterPanelProps {
92
+ children?: ReactNode;
93
+ }
94
+ interface IListingSearchProps {
95
+ value: string;
96
+ onChange(value: string): void;
97
+ placeholder?: string;
98
+ }
99
+ interface IListingEmptyProps {
100
+ }
101
+ interface IListingLoadingProps {
102
+ }
103
+ interface IListingResultHeaderProps {
104
+ count: number;
105
+ total?: number;
106
+ }
107
+ interface IListingToolbarProps {
108
+ children?: ReactNode;
109
+ }
110
+ interface IListingComponents {
111
+ Card: ComponentType<IListingCardProps>;
112
+ Marker: ComponentType<IListingMarkerProps>;
113
+ Popup: ComponentType<IListingPopupProps>;
114
+ Sidebar: ComponentType<IListingSidebarProps>;
115
+ FilterPanel: ComponentType<IListingFilterPanelProps>;
116
+ Search: ComponentType<IListingSearchProps>;
117
+ Empty: ComponentType<IListingEmptyProps>;
118
+ Loading: ComponentType<IListingLoadingProps>;
119
+ ResultHeader: ComponentType<IListingResultHeaderProps>;
120
+ Toolbar: ComponentType<IListingToolbarProps>;
121
+ }
122
+ /**
123
+ * Injection point for custom-component overrides. Every slot is optional —
124
+ * anything not provided falls back to the (unstyled) default. Mirrors
125
+ * `react-wizard-engine`'s `WizardComponentsProvider`: explicit per-slot
126
+ * `provided ?? defaults.X` merge, not a generic/reflective loop.
127
+ */
128
+ declare function ListingComponentsProvider(props: Partial<IListingComponents> & {
129
+ children: ReactNode;
130
+ }): react.JSX.Element;
131
+ declare function useListingComponents(): IListingComponents;
132
+
133
+ type BottomNavView = 'list' | 'map';
134
+ /**
135
+ * An action button in the mobile chrome (e.g. "Add"/"Save"). Despite the
136
+ * name (kept for API compatibility), it renders in `MobileHeader`, not here.
137
+ */
138
+ interface IBottomNavAction {
139
+ label: string;
140
+ icon?: ReactNode;
141
+ onClick(): void;
142
+ }
143
+ interface IBottomNavProps {
144
+ view: BottomNavView;
145
+ onViewChange(view: BottomNavView): void;
146
+ }
147
+ /**
148
+ * Mobile-only bottom navigation (`.rle-bottom-nav`, CSS-hidden above the
149
+ * mobile breakpoint -- see `styles.css`'s layout-shell section, the single
150
+ * source of truth for it): a floating **List | Map** segmented toggle
151
+ * (`.rle-viewtoggle`) that drives which of `StyledListingLayout`'s two
152
+ * full-area panels is visible. The **Filters** button and the optional caller
153
+ * action (e.g. "Save") live in the mobile header (`MobileHeader`) instead, so
154
+ * the footer pill carries the view toggle alone. No lucide/icon library
155
+ * dependency -- every icon here is a small inline SVG, matching the rest of
156
+ * `/styled`'s zero-extra-dependency policy.
157
+ */
158
+ declare function BottomNav({ view, onViewChange }: IBottomNavProps): react.JSX.Element;
159
+
160
+ interface IStyledListingLayoutProps {
161
+ /**
162
+ * Optional header search box, LIBRARY-wired: name the `TFilters` field it
163
+ * drives via
164
+ * `filterKey`, and the layout reads the current value from the engine's
165
+ * filters and writes edits back with `applyFilters({ [filterKey]: value ||
166
+ * undefined })`. Rendered in BOTH the desktop filter bar and the mobile
167
+ * header. Do NOT also register `filterKey` as a `ListingFilters` control --
168
+ * it would then render twice. Omitted entirely when not passed.
169
+ */
170
+ search?: {
171
+ filterKey: string;
172
+ placeholder?: string;
173
+ };
174
+ /** Extra content rendered in `.rle-list-header` (above the list), to the right of `ListingResultHeader` (e.g. a sort control + save-search). */
175
+ toolbarEnd?: ReactNode;
176
+ /** Optional mobile-header action button (e.g. "Save"), forwarded verbatim to `<MobileHeader action={...} />`. Omit to render just the search + Filters button there. */
177
+ mobileAction?: IBottomNavAction;
178
+ /**
179
+ * Whether a map is configured. When `false`, the map region is dropped and
180
+ * the results list fills the full width as a multi-column grid, and the
181
+ * mobile List|Map toggle is omitted (nothing to toggle to). Defaults to
182
+ * `engine.map != null` -- the engine is the source of truth, so this only
183
+ * needs passing to override that (e.g. to hide a configured map).
184
+ */
185
+ hasMap?: boolean;
186
+ /** Whether the layout fetches the first page itself on mount (`engine.applyFilters({})`). Defaults to `true`. */
187
+ autoFetch?: boolean;
188
+ /** Forwarded verbatim to `<ListingMap center={mapCenter} />` -- see that component's "Auto-fit" doc comment. */
189
+ mapCenter?: LatLng;
190
+ /** Forwarded verbatim to `<ListingMap zoom={mapZoom} />`. */
191
+ mapZoom?: number;
192
+ className?: string;
193
+ }
194
+ /**
195
+ * Full, responsive, Tailwind-free listing experience -- built from the
196
+ * structure-only compound components (`~/react`) plus the injected
197
+ * `Styled*` slot components, with a rich mobile experience: a bottom
198
+ * nav (`BottomNav`) and a bottom sheet (`BottomSheet`) for filters. Every
199
+ * class used here is one of
200
+ * the `.rle-*` layout classes added to `styles.css` alongside this file --
201
+ * no Tailwind, no inline styles, no other stylesheet required.
202
+ *
203
+ * STRUCTURE (`.rle-app`) -- the mobile/desktop breakpoint and the grid's
204
+ * column sizing live in ONE place, `styles.css`'s layout-shell section; the
205
+ * bullets below say only which side of it each piece renders on:
206
+ * - `.rle-filter-bar` (desktop only, CSS-hidden below the breakpoint): the
207
+ * optional `Search` slot and `<ListingFilters>` laid out as a single
208
+ * horizontally-scrolling row (`className="rle-filters-row"`,
209
+ * `groupClassName="rle-filter-group"`) with edge fades. The result header +
210
+ * `toolbarEnd` are NOT here -- they sit in `.rle-list-header` above the list.
211
+ * - `.rle-list-header` (top of `.rle-list`): `<ListingResultHeader>` (title +
212
+ * count) at the left, `toolbarEnd` (sort control, save-search, ...) at the
213
+ * right -- a heading for the results, on every viewport.
214
+ * - `.rle-body.rle-split`: a list-majority list|map CSS grid from the
215
+ * breakpoint up; below it, a single full-area panel with exactly one of
216
+ * `.rle-list`/`.rle-map` visible at a time via `data-mobile-view` (see
217
+ * `styles.css`'s mobile media query). Both regions stay mounted at ALL
218
+ * times regardless of viewport/toggle state -- only their visibility flips
219
+ * -- so neither `ListingList` nor `ListingMap` remounts (and re-triggers
220
+ * its own mount effects) on toggle or resize.
221
+ * - `<MobileHeader>` (mobile only): the same search box, a **Filters** button
222
+ * (opens the `<BottomSheet>`, with an applied-filter count badge) and the
223
+ * optional `mobileAction`.
224
+ * - `<BottomNav>` (mobile only): the floating List|Map view toggle, wired to
225
+ * the same `mobileView` state as the CSS toggle. Omitted when there is no
226
+ * map (nothing to toggle to).
227
+ * - `<BottomSheet title="Filters">`: the SAME `<ListingFilters>` component,
228
+ * stacked vertically (`className="rle-filter-stack"`) for the sheet's
229
+ * narrower body, plus a footer with "Clear all" (applies
230
+ * `engine.filters.clearedParams()` -- see that method's doc for why a reset
231
+ * round-trips each def's to/fromParams) and "Show N results" (just closes
232
+ * the sheet -- every filter control already applies live via its own
233
+ * `onChange`, so there is nothing left to commit).
234
+ * - Fetches the first page itself on mount (`engine.applyFilters({})`) by
235
+ * default -- pass `autoFetch={false}`
236
+ * to opt out and drive the first fetch yourself.
237
+ */
238
+ declare function StyledListingLayout({ search, toolbarEnd, mobileAction, autoFetch, hasMap: hasMapProp, mapCenter, mapZoom, className, }: IStyledListingLayoutProps): react.JSX.Element;
239
+
240
+ /**
241
+ * Two-shape `map` prop: pass a ready `MapProvider`, or the `{ apiKey, mapId? }`
242
+ * shorthand. The shorthand is resolved via a DYNAMIC `import()` of
243
+ * `~/maps/google` (see `useResolvedMap` below) rather than a static one, so an
244
+ * app that never sets `map` doesn't pull `@googlemaps/js-api-loader` into its
245
+ * bundle.
246
+ */
247
+ type ListingAppMapProp = {
248
+ provider: MapProvider;
249
+ center?: LatLng;
250
+ zoom?: number;
251
+ } | {
252
+ apiKey: string;
253
+ mapId?: string;
254
+ center?: LatLng;
255
+ zoom?: number;
256
+ };
257
+ interface ListingAppProps<TFilters> {
258
+ /**
259
+ * One or more marker-layer datasets; the FIRST entry is the primary
260
+ * dataset (drives the results list + pagination) -- same "insertion
261
+ * order" rule `composeListingProviders`/`withDataset` already follow.
262
+ * Entity-erased (`any`, not `TEntity`) for the same reason
263
+ * `DatasetRegistry<unknown, TFilters>` is: one array can legitimately hold
264
+ * heterogeneous layers (a properties dataset and a businesses dataset have
265
+ * different entity types) -- see `compose-listing-providers.ts`'s
266
+ * `withDataset` doc comment.
267
+ */
268
+ datasets: DatasetDefinition<any, TFilters>[];
269
+ /** A `(reg) => { reg.add(...); }` callback that registers your filters -- forwarded verbatim to `withFilters`. */
270
+ filters?: (reg: FilterRegistry<TFilters>) => void;
271
+ /** A ready `MapProvider`, or `{ apiKey, mapId? }` to build a `googleProvider` internally. Omit for no map -- the layout drops the map region and renders the list full-width. */
272
+ map?: ListingAppMapProp;
273
+ /** Slot overrides, merged OVER the `/styled` defaults -- see this file's doc comment for how the merge works. */
274
+ components?: Partial<IListingComponents>;
275
+ /**
276
+ * Hydrates the engine's initial filters on mount -- typically parsed from
277
+ * the CONSUMER's own URL (your own query-string parser over `new
278
+ * URLSearchParams(window.location.search)`), but any source works. Half
279
+ * of the event-based URL API this component exposes: `ListingApp` never
280
+ * reads `window.location` itself, it only accepts filters as a prop.
281
+ */
282
+ initialFilters?: TFilters;
283
+ /**
284
+ * EVENT OUT: fires with the engine's current filters (`TFilters`, not the
285
+ * store's `DeepReadonly` wrapper) every time `ListingEventType.FiltersChanged`
286
+ * fires on the underlying engine -- i.e. on every `engine.applyFilters(...)`
287
+ * call, INCLUDING the one `StyledListingLayout` makes on mount when
288
+ * `autoFetch` is on (a `{}` patch that still round-trips through
289
+ * `currentFilters()` and re-emits the very `initialFilters` this component
290
+ * was given). That first call is intentionally NOT suppressed: a
291
+ * consumer's handler is expected to write filters back to a URL via
292
+ * `history.replaceState` (or a router's equivalent), and replacing the URL
293
+ * with the SAME query string it already hydrated from is a no-op, not a
294
+ * visible echo -- so "just always emit" is simpler than tracking a
295
+ * first-call flag for no behavioral gain. This is the OTHER half of the
296
+ * event-based URL API: `ListingApp` (and the engine underneath it) never
297
+ * touches `window.history` -- the CONSUMER owns routing, driven by this
298
+ * callback plus `initialFilters` above. `UrlSyncController`/`BrowserHistoryPort`
299
+ * (`~/core`, `~/react`) remain exported as optional, lower-level helpers
300
+ * for consumers who want the library to own `window.history` directly, but
301
+ * they are no longer the primary/recommended path for new integrations.
302
+ */
303
+ onFiltersChange?: (filters: TFilters) => void;
304
+ /** The mobile header action (e.g. "Save"/"Add") -- forwarded verbatim to `StyledListingLayout`/`MobileHeader`. */
305
+ mobileAction?: IBottomNavAction;
306
+ search?: IStyledListingLayoutProps['search'];
307
+ toolbarEnd?: IStyledListingLayoutProps['toolbarEnd'];
308
+ config?: Partial<IListingConfigOptions>;
309
+ autoFetch?: boolean;
310
+ className?: string;
311
+ }
312
+ /**
313
+ * Turnkey, batteries-included, Tailwind-free entry point -- and (per
314
+ * `src/index.ts`) the package's MAIN-ENTRY default: `import { ListingApp } from
315
+ * 'react-listing-engine'` gives you this component. Pass datasets + filters +
316
+ * a map + component overrides, and `ListingApp` composes
317
+ * `composeListingProviders(...)`, `<ListingProvider>`, the `/styled` defaults,
318
+ * and `<StyledListingLayout>` for you.
319
+ *
320
+ * `TEntity` still can't be inferred from these props (`datasets` narrows it,
321
+ * but a multi-dataset array widens back to the union/`unknown` in practice)
322
+ * -- annotate the call site when entity-level typing matters, exactly like
323
+ * `<ListingProvider<TEntity, TFilters>>` itself.
324
+ *
325
+ * COMPONENTS MERGE: `components` overrides are applied ON TOP of the
326
+ * `/styled` defaults via ONE explicit `{ ...styledDefaultComponents,
327
+ * ...components }` object, passed to a single `<ListingComponentsProvider>`
328
+ * -- never as a nested `<ListingComponentsProvider>` inside
329
+ * `StyledComponentsProviderWithDefaults`. `ListingComponentsProvider` merges
330
+ * `provided ?? ITS OWN private, unstyled fallbacks` per slot (it does not
331
+ * read the parent context -- see `src/react/components-provider.tsx`), so
332
+ * nesting would silently discard every un-overridden `/styled` default
333
+ * instead of keeping it.
334
+ *
335
+ * URL SYNC IS EVENT-BASED, NOT INTERNAL: this component takes no `urlSync`
336
+ * prop and never constructs/starts a
337
+ * `UrlSyncController` -- it never touches `window.history`. Instead it
338
+ * accepts `initialFilters` (hydrate FROM the consumer's URL) and emits
339
+ * `onFiltersChange` (write TO the consumer's URL/router) -- see both props'
340
+ * doc comments for the full contract. This keeps the library's surface
341
+ * router-agnostic (a plain `history.replaceState`, a Next.js `router.replace`,
342
+ * a React Router `setSearchParams`, etc. all work identically from the
343
+ * consumer's side) instead of assuming `window.history` is the right target.
344
+ */
345
+ declare function ListingApp<TEntity, TFilters>(props: ListingAppProps<TFilters>): react.JSX.Element | null;
346
+
347
+ export { BottomNav as B, type ClusterOptions as C, type DatasetDefinition as D, FilterRegistry as F, type IListingConfigOptions as I, ListingApp as L, type MarkerRenderer as M, PaginationMode as P, StyledListingLayout as S, type IListingToolbarProps as a, type FilterControlProps as b, type FilterDefinition as c, type IListingCardProps as d, type IListingComponents as e, type IListingEmptyProps as f, type IListingFilterPanelProps as g, type IListingLoadingProps as h, type IListingMarkerProps as i, type IListingPopupProps as j, type IListingResultHeaderProps as k, type IListingSearchProps as l, type IListingSidebarProps as m, type ListingAppProps as n, ListingComponentsProvider as o, type BottomNavView as p, type IBottomNavAction as q, type IBottomNavProps as r, type IStyledListingLayoutProps as s, type ListingAppMapProp as t, useListingComponents as u };