react-listing-engine 0.6.5 → 0.6.6

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.
@@ -1,259 +0,0 @@
1
- import * as react from 'react';
2
- import { ReactNode } from 'react';
3
- import { 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, a as IListingToolbarProps, D as DatasetDefinition, F as FilterRegistry, I as IListingConfigOptions } from '../components-provider-FinfaqUT.cjs';
4
- import { L as LatLng, a as MapProvider } from '../map-provider.interface-DT-v1plm.cjs';
5
- import { U as UrlSyncController } from '../url-sync.controller-DXSIWgTa.cjs';
6
- import { ClassValue } from 'clsx';
7
-
8
- /**
9
- * Convenience wrapper around `ListingComponentsProvider` that wires in every
10
- * package-shipped `Default*` component (via `shadcnDefaultComponents`, the
11
- * shared source of truth also used by `ListingApp`'s merge). Use this if you
12
- * want the styled adapter out of the box; use `ListingComponentsProvider`
13
- * directly (with your own components for some or all slots) otherwise -- the
14
- * two compose fine since `ListingComponentsProvider` falls back per-slot.
15
- * Mirrors `react-wizard-engine`'s `WizardComponentsProviderWithDefaults`.
16
- */
17
- declare function ListingComponentsProviderWithDefaults({ children }: {
18
- children: ReactNode;
19
- }): react.JSX.Element;
20
-
21
- /**
22
- * Default styled `Card` slot. Renders an optional image, title, subtitle,
23
- * price and badge from a plain view-model item, and doubles as the
24
- * clickable/selectable surface (`onSelect`) -- when `onSelect` is provided
25
- * the card is a `button` rather than a `div` + click handler so it is
26
- * keyboard operable and announces as a toggle (`aria-pressed`) for free.
27
- * When `onSelect` is absent (display-only usage) it renders as a
28
- * non-interactive `<article>` with the same visual classes, minus the
29
- * button/focus/aria-pressed semantics -- so a purely presentational card
30
- * doesn't add a no-op tab stop.
31
- */
32
- declare function DefaultCard({ item, selected, onSelect }: IListingCardProps): react.JSX.Element;
33
-
34
- /**
35
- * Every `/shadcn` styled default, keyed by slot -- the single source of truth
36
- * both `ListingComponentsProviderWithDefaults` (spreads it verbatim) and
37
- * `ListingApp` (spreads a `components` override OVER it) build on.
38
- *
39
- * Kept as a plain object, not JSX, so a caller can merge it with
40
- * `{ ...shadcnDefaultComponents, ...overrides }` and hand the result to ONE
41
- * `ListingComponentsProvider`. This matters because `ListingComponentsProvider`
42
- * merges `provided ?? ITS OWN private, UNSTYLED fallbacks` per slot -- it does
43
- * not read the parent context (see `src/react/components-provider.tsx`).
44
- * Nesting a `ListingComponentsProvider` (with only the overrides) INSIDE a
45
- * `ListingComponentsProviderWithDefaults` would therefore silently reset
46
- * every un-overridden slot back to the bare fallback instead of the shadcn
47
- * default -- exactly the bug this object exists to avoid.
48
- */
49
- declare const shadcnDefaultComponents: IListingComponents;
50
-
51
- /** Default styled `Empty` slot: a centered empty state with an icon, heading and hint. */
52
- declare function DefaultEmpty(_props: IListingEmptyProps): react.JSX.Element;
53
-
54
- /** Default styled `FilterPanel` slot: stacks one filter control per row inside `Sidebar`. */
55
- declare function DefaultFilterPanel({ children }: IListingFilterPanelProps): react.JSX.Element;
56
-
57
- /** Default styled `Loading` slot: a shimmering skeleton list, announced via `role="status"`. */
58
- declare function DefaultLoading(_props: IListingLoadingProps): react.JSX.Element;
59
-
60
- /** Default styled `Marker` slot: a small price pill for a map point. */
61
- declare function DefaultMarker({ point }: IListingMarkerProps): react.JSX.Element;
62
-
63
- /**
64
- * Default styled `Popup` slot: a small card with an accessible close button.
65
- * `role="group"` + `aria-label` (rather than `role="dialog"`) since this is a
66
- * non-modal, non-focus-trapped popup anchored to a map marker -- `dialog`
67
- * without modality/focus management would misrepresent it to AT users.
68
- */
69
- declare function DefaultPopup({ entity, onClose }: IListingPopupProps): react.JSX.Element;
70
-
71
- /** Default styled `ResultHeader` slot: `"{count} results"`, or `"{count} of {total} results"` when a distinct total is known. */
72
- declare function DefaultResultHeader({ count, total }: IListingResultHeaderProps): react.JSX.Element;
73
-
74
- /** Default styled `Search` slot: a plain styled text input. */
75
- declare function DefaultSearch({ value, onChange, placeholder }: IListingSearchProps): react.JSX.Element;
76
-
77
- /**
78
- * Default styled `Sidebar` slot: the outer chrome for the filters column in
79
- * `ListingLayout`. Full width and unbordered on mobile (stacks above the
80
- * results in normal document flow); becomes a fixed-width bordered rail from
81
- * `md` up.
82
- */
83
- declare function DefaultSidebar({ children }: IListingSidebarProps): react.JSX.Element;
84
-
85
- /** Default styled `Toolbar` slot: a bordered bar wrapping arbitrary children. */
86
- declare function DefaultToolbar({ children }: IListingToolbarProps): react.JSX.Element;
87
-
88
- interface IListingLayoutProps {
89
- /**
90
- * Optional search box, wired by the CONSUMER into their own `TFilters`
91
- * shape. `ListingLayout` is filters-shape-erased (same reasoning as the
92
- * engine itself -- see `ListingEngineOptions`'s docstring) so it cannot
93
- * derive a generic "search" filter on its own; when omitted, no search box
94
- * is rendered at all. Documented, deliberate scope: wiring a named
95
- * "search" filter convention end-to-end is a future enhancement.
96
- */
97
- search?: {
98
- value: string;
99
- onChange: (value: string) => void;
100
- placeholder?: string;
101
- };
102
- className?: string;
103
- /** Extra content rendered at the end of the filter bar, alongside `ListingResultHeader` (e.g. a sort control). */
104
- toolbarEnd?: ReactNode;
105
- /**
106
- * Whether `ListingLayout` fetches the first page itself on mount
107
- * (`engine.applyFilters({})`). Defaults to `true` (batteries-included).
108
- * Pass `false` to defer the initial fetch to the caller -- e.g. a
109
- * search-before-results flow, or when `urlSync` hydrates filters from the
110
- * URL and should drive the first fetch instead of racing it.
111
- */
112
- autoFetch?: boolean;
113
- /**
114
- * Forwarded verbatim to `<ListingMap center={mapCenter} />`. Omit to get
115
- * `ListingMap`'s turnkey auto-fit default (frames the map to its own data
116
- * once it loads) -- passing `mapCenter` opts OUT of auto-fit entirely, on
117
- * the theory that an explicit initial view is a deliberate choice
118
- * (see `ListingMap`'s doc comment's "Auto-fit" section).
119
- */
120
- mapCenter?: LatLng;
121
- /** Forwarded verbatim to `<ListingMap zoom={mapZoom} />`. See `mapCenter`. */
122
- mapZoom?: number;
123
- }
124
- /**
125
- * Full, responsive default listing experience, matching the "top filter bar
126
- * + list-left/map-right split" pattern of a typical `/find` real-estate
127
- * search page: a sticky horizontal filter bar (search + filter groups +
128
- * result count), then below it a split -- a scrollable card grid on the left
129
- * and a full-height map on the right. Composed entirely from the
130
- * structure-only compound components in `~/react` plus the injected slot
131
- * components (via `useListingComponents()` for `Search`, and implicitly
132
- * through each compound component for `Card`/`Empty`/`Loading`/etc.) -- this
133
- * file adds layout/chrome only, no new business logic.
134
- *
135
- * Layout choices (documented, not the only valid ones):
136
- * - FILTER BAR (`data-slot="listing-layout-filter-bar"`): `sticky top-0`,
137
- * `border-b`/`bg-background`, and a `flex flex-wrap items-end gap-3` row
138
- * containing the injected `Search` (when `search` is passed),
139
- * `<ListingFilters>` (given the same horizontal row className plus a
140
- * `min-w-0` `groupClassName` so individual groups can shrink instead of
141
- * forcing overflow), and a trailing `ml-auto` cluster with
142
- * `<ListingResultHeader>` + `toolbarEnd`. `flex-wrap` is what makes this
143
- * gracefully reflow on narrow widths -- groups drop to new lines instead
144
- * of overflowing or requiring a separate mobile-only layout.
145
- * - SPLIT (`data-slot="listing-layout-split"`): a single CSS grid,
146
- * `md:grid-cols-[minmax(340px,42%)_1fr]` from `md` up (list column floors
147
- * at 340px, caps at 42% of the split's width; map takes the rest). Below
148
- * `md` there is no grid -- just two full-width panels, and exactly one is
149
- * visible at a time (see the mobile toggle below).
150
- * - LIST region (`data-slot="listing-layout-list"`): `overflow-y-auto`, its
151
- * own scroll container so browsing the list never requires scrolling the
152
- * map out of view. Holds `<ListingList>` (an auto-fill card grid) and
153
- * `<ListingPagination>`.
154
- * - MAP region (`data-slot="listing-layout-map"`): fills the split's full
155
- * height via the grid's default stretch alignment (no explicit height
156
- * needed), plus `md:sticky md:top-0` so that if a consuming app ever lets
157
- * `ListingLayout` sit inside a naturally document-scrolling page (rather
158
- * than the fixed-height shell this component defaults to via
159
- * `h-full min-h-0`), the map still pins in place while the list scrolls
160
- * past -- inert (but harmless) in the default fixed-height composition,
161
- * where there is nothing above the split to scroll past in the first
162
- * place. Renders `<ListingMap>` with a default centered fallback message
163
- * (`MAP_FALLBACK`) for when no `MapProvider` is configured, so the pane
164
- * never looks broken/blank.
165
- * - MOBILE LIST/MAP TOGGLE (`data-slot="listing-layout-mobile-toggle"`,
166
- * `md:hidden`): a small two-button segmented control, local `useState`
167
- * (`mobileView`), defaulting to `'list'`. Both the list and map regions
168
- * stay mounted at all times (never remounted on toggle -- that would
169
- * re-trigger `ListingList`'s/`ListingMap`'s own mount effects for no
170
- * reason); only their visibility flips via `hidden md:block` /
171
- * `block md:block`, i.e. the toggle only ever matters below `md` -- at
172
- * `md` and up both regions are always visible side by side and the toggle
173
- * control itself is hidden.
174
- * - Fetches the first page itself on mount (`engine.applyFilters({})`) by
175
- * default: none of the structure-only `~/react` compound components do
176
- * this (they are deliberately side-effect-free), so as the
177
- * batteries-included "full experience" entry point, `ListingLayout` is the
178
- * natural, single owner of that one bootstrapping side effect. Pass
179
- * `autoFetch={false}` to opt out and drive the first fetch yourself (e.g.
180
- * search-before-results, or when `urlSync` hydrates filters from the URL).
181
- */
182
- declare function ListingLayout({ search, className, toolbarEnd, autoFetch, mapCenter, mapZoom, }: IListingLayoutProps): react.JSX.Element;
183
-
184
- /**
185
- * `ListingApp.map` accepts either a ready-made `MapProvider` (any
186
- * implementation -- Google, a fake, a future provider) or, as a convenience,
187
- * a bare Google Maps API key. See `useResolvedMap`'s doc comment for why the
188
- * key shorthand is resolved via a dynamic `import()` rather than a static one.
189
- *
190
- * Both shapes optionally carry `center`/`zoom` -- the initial view, forwarded
191
- * through `ListingLayout`'s `mapCenter`/`mapZoom` straight into
192
- * `ListingMap`'s own `center`/`zoom` props. Omit them to get `ListingMap`'s
193
- * turnkey auto-fit default instead (see that component's doc comment);
194
- * supplying `center` opts out of auto-fit entirely.
195
- */
196
- type ListingAppMapProp = {
197
- provider: MapProvider;
198
- center?: LatLng;
199
- zoom?: number;
200
- } | {
201
- apiKey: string;
202
- mapId?: string;
203
- center?: LatLng;
204
- zoom?: number;
205
- };
206
- interface ListingAppProps<TFilters> {
207
- /**
208
- * One or more marker-layer datasets; the FIRST entry is the primary
209
- * dataset (drives the results list + pagination) -- same "insertion order"
210
- * rule `composeListingProviders`/`withDataset` already follow. Entity-erased
211
- * (`any`, not `TEntity`) for the same reason `DatasetRegistry<unknown,
212
- * TFilters>` is: one array can legitimately hold heterogeneous layers (a
213
- * properties dataset and a businesses dataset have different entity types)
214
- * -- see `compose-listing-providers.ts`'s `withDataset` doc comment.
215
- */
216
- datasets: DatasetDefinition<any, TFilters>[];
217
- /** A `(reg) => { reg.add(...); }` callback that registers your filters -- forwarded verbatim to `withFilters`. */
218
- filters?: (reg: FilterRegistry<TFilters>) => void;
219
- /** A ready `MapProvider`, or `{ apiKey, mapId? }` to build a `googleProvider` internally. Omit for no map (the styled layout shows a "Map unavailable" fallback). */
220
- map?: ListingAppMapProp;
221
- /** Slot overrides, merged OVER the shadcn styled defaults -- see this file's doc comment for how the merge works. */
222
- components?: Partial<IListingComponents>;
223
- /** A `UrlSyncController<TFilters>`, or `true`. `true` is a documented no-op: the library is filters-shape-erased and cannot generically derive a `TFilters <-> QueryParams` mapping, so build a real controller for your own filter shape. */
224
- urlSync?: UrlSyncController<TFilters> | boolean;
225
- initialFilters?: TFilters;
226
- config?: Partial<IListingConfigOptions>;
227
- className?: string;
228
- search?: IListingLayoutProps['search'];
229
- autoFetch?: boolean;
230
- }
231
- /**
232
- * Turnkey, batteries-included entry point: pass datasets + filters + a map +
233
- * component overrides + URL sync, and `ListingApp` composes
234
- * `composeListingProviders(...)`, `<ListingProvider>`, the styled `/shadcn`
235
- * defaults, and `<ListingLayout>` for you. Equivalent to (and internally
236
- * built from) the lower-level primitives every other `/shadcn` example wires
237
- * by hand -- reach for those directly when you need something `ListingApp`
238
- * doesn't expose (e.g. multiple `<ListingLayout>`s sharing one engine).
239
- *
240
- * `TEntity` still can't be inferred from these props (`datasets` narrows it,
241
- * but a multi-dataset array widens back to the union/`unknown` in practice)
242
- * -- annotate the call site when entity-level typing matters, exactly like
243
- * `<ListingProvider<TEntity, TFilters>>` itself.
244
- *
245
- * COMPONENTS MERGE: `components` overrides are applied ON TOP of the shadcn
246
- * styled defaults via ONE explicit `{ ...shadcnDefaultComponents, ...components }`
247
- * object, passed to a single `<ListingComponentsProvider>` -- never as a
248
- * nested `<ListingComponentsProvider>` inside `<ListingComponentsProviderWithDefaults>`.
249
- * `ListingComponentsProvider` merges `provided ?? ITS OWN private, unstyled
250
- * fallbacks` per slot (it does not read the parent context -- see
251
- * `src/react/components-provider.tsx`), so nesting would silently discard
252
- * every un-overridden shadcn default instead of keeping it.
253
- */
254
- declare function ListingApp<TEntity, TFilters>(props: ListingAppProps<TFilters>): react.JSX.Element | null;
255
-
256
- /** Merges class-name fragments, letting `tailwind-merge` resolve conflicting Tailwind utilities (last one wins). Mirrors `react-wizard-engine`'s `~/utils/cn`. */
257
- declare function cn(...inputs: ClassValue[]): string;
258
-
259
- export { DefaultCard, DefaultEmpty, DefaultFilterPanel, DefaultLoading, DefaultMarker, DefaultPopup, DefaultResultHeader, DefaultSearch, DefaultSidebar, DefaultToolbar, type IListingLayoutProps, ListingApp, type ListingAppMapProp, type ListingAppProps, ListingComponentsProviderWithDefaults, ListingLayout, cn, shadcnDefaultComponents };
@@ -1,259 +0,0 @@
1
- import * as react from 'react';
2
- import { ReactNode } from 'react';
3
- import { 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, a as IListingToolbarProps, D as DatasetDefinition, F as FilterRegistry, I as IListingConfigOptions } from '../components-provider-cWVWEDXC.js';
4
- import { L as LatLng, a as MapProvider } from '../map-provider.interface-DT-v1plm.js';
5
- import { U as UrlSyncController } from '../url-sync.controller-DK5W_0OZ.js';
6
- import { ClassValue } from 'clsx';
7
-
8
- /**
9
- * Convenience wrapper around `ListingComponentsProvider` that wires in every
10
- * package-shipped `Default*` component (via `shadcnDefaultComponents`, the
11
- * shared source of truth also used by `ListingApp`'s merge). Use this if you
12
- * want the styled adapter out of the box; use `ListingComponentsProvider`
13
- * directly (with your own components for some or all slots) otherwise -- the
14
- * two compose fine since `ListingComponentsProvider` falls back per-slot.
15
- * Mirrors `react-wizard-engine`'s `WizardComponentsProviderWithDefaults`.
16
- */
17
- declare function ListingComponentsProviderWithDefaults({ children }: {
18
- children: ReactNode;
19
- }): react.JSX.Element;
20
-
21
- /**
22
- * Default styled `Card` slot. Renders an optional image, title, subtitle,
23
- * price and badge from a plain view-model item, and doubles as the
24
- * clickable/selectable surface (`onSelect`) -- when `onSelect` is provided
25
- * the card is a `button` rather than a `div` + click handler so it is
26
- * keyboard operable and announces as a toggle (`aria-pressed`) for free.
27
- * When `onSelect` is absent (display-only usage) it renders as a
28
- * non-interactive `<article>` with the same visual classes, minus the
29
- * button/focus/aria-pressed semantics -- so a purely presentational card
30
- * doesn't add a no-op tab stop.
31
- */
32
- declare function DefaultCard({ item, selected, onSelect }: IListingCardProps): react.JSX.Element;
33
-
34
- /**
35
- * Every `/shadcn` styled default, keyed by slot -- the single source of truth
36
- * both `ListingComponentsProviderWithDefaults` (spreads it verbatim) and
37
- * `ListingApp` (spreads a `components` override OVER it) build on.
38
- *
39
- * Kept as a plain object, not JSX, so a caller can merge it with
40
- * `{ ...shadcnDefaultComponents, ...overrides }` and hand the result to ONE
41
- * `ListingComponentsProvider`. This matters because `ListingComponentsProvider`
42
- * merges `provided ?? ITS OWN private, UNSTYLED fallbacks` per slot -- it does
43
- * not read the parent context (see `src/react/components-provider.tsx`).
44
- * Nesting a `ListingComponentsProvider` (with only the overrides) INSIDE a
45
- * `ListingComponentsProviderWithDefaults` would therefore silently reset
46
- * every un-overridden slot back to the bare fallback instead of the shadcn
47
- * default -- exactly the bug this object exists to avoid.
48
- */
49
- declare const shadcnDefaultComponents: IListingComponents;
50
-
51
- /** Default styled `Empty` slot: a centered empty state with an icon, heading and hint. */
52
- declare function DefaultEmpty(_props: IListingEmptyProps): react.JSX.Element;
53
-
54
- /** Default styled `FilterPanel` slot: stacks one filter control per row inside `Sidebar`. */
55
- declare function DefaultFilterPanel({ children }: IListingFilterPanelProps): react.JSX.Element;
56
-
57
- /** Default styled `Loading` slot: a shimmering skeleton list, announced via `role="status"`. */
58
- declare function DefaultLoading(_props: IListingLoadingProps): react.JSX.Element;
59
-
60
- /** Default styled `Marker` slot: a small price pill for a map point. */
61
- declare function DefaultMarker({ point }: IListingMarkerProps): react.JSX.Element;
62
-
63
- /**
64
- * Default styled `Popup` slot: a small card with an accessible close button.
65
- * `role="group"` + `aria-label` (rather than `role="dialog"`) since this is a
66
- * non-modal, non-focus-trapped popup anchored to a map marker -- `dialog`
67
- * without modality/focus management would misrepresent it to AT users.
68
- */
69
- declare function DefaultPopup({ entity, onClose }: IListingPopupProps): react.JSX.Element;
70
-
71
- /** Default styled `ResultHeader` slot: `"{count} results"`, or `"{count} of {total} results"` when a distinct total is known. */
72
- declare function DefaultResultHeader({ count, total }: IListingResultHeaderProps): react.JSX.Element;
73
-
74
- /** Default styled `Search` slot: a plain styled text input. */
75
- declare function DefaultSearch({ value, onChange, placeholder }: IListingSearchProps): react.JSX.Element;
76
-
77
- /**
78
- * Default styled `Sidebar` slot: the outer chrome for the filters column in
79
- * `ListingLayout`. Full width and unbordered on mobile (stacks above the
80
- * results in normal document flow); becomes a fixed-width bordered rail from
81
- * `md` up.
82
- */
83
- declare function DefaultSidebar({ children }: IListingSidebarProps): react.JSX.Element;
84
-
85
- /** Default styled `Toolbar` slot: a bordered bar wrapping arbitrary children. */
86
- declare function DefaultToolbar({ children }: IListingToolbarProps): react.JSX.Element;
87
-
88
- interface IListingLayoutProps {
89
- /**
90
- * Optional search box, wired by the CONSUMER into their own `TFilters`
91
- * shape. `ListingLayout` is filters-shape-erased (same reasoning as the
92
- * engine itself -- see `ListingEngineOptions`'s docstring) so it cannot
93
- * derive a generic "search" filter on its own; when omitted, no search box
94
- * is rendered at all. Documented, deliberate scope: wiring a named
95
- * "search" filter convention end-to-end is a future enhancement.
96
- */
97
- search?: {
98
- value: string;
99
- onChange: (value: string) => void;
100
- placeholder?: string;
101
- };
102
- className?: string;
103
- /** Extra content rendered at the end of the filter bar, alongside `ListingResultHeader` (e.g. a sort control). */
104
- toolbarEnd?: ReactNode;
105
- /**
106
- * Whether `ListingLayout` fetches the first page itself on mount
107
- * (`engine.applyFilters({})`). Defaults to `true` (batteries-included).
108
- * Pass `false` to defer the initial fetch to the caller -- e.g. a
109
- * search-before-results flow, or when `urlSync` hydrates filters from the
110
- * URL and should drive the first fetch instead of racing it.
111
- */
112
- autoFetch?: boolean;
113
- /**
114
- * Forwarded verbatim to `<ListingMap center={mapCenter} />`. Omit to get
115
- * `ListingMap`'s turnkey auto-fit default (frames the map to its own data
116
- * once it loads) -- passing `mapCenter` opts OUT of auto-fit entirely, on
117
- * the theory that an explicit initial view is a deliberate choice
118
- * (see `ListingMap`'s doc comment's "Auto-fit" section).
119
- */
120
- mapCenter?: LatLng;
121
- /** Forwarded verbatim to `<ListingMap zoom={mapZoom} />`. See `mapCenter`. */
122
- mapZoom?: number;
123
- }
124
- /**
125
- * Full, responsive default listing experience, matching the "top filter bar
126
- * + list-left/map-right split" pattern of a typical `/find` real-estate
127
- * search page: a sticky horizontal filter bar (search + filter groups +
128
- * result count), then below it a split -- a scrollable card grid on the left
129
- * and a full-height map on the right. Composed entirely from the
130
- * structure-only compound components in `~/react` plus the injected slot
131
- * components (via `useListingComponents()` for `Search`, and implicitly
132
- * through each compound component for `Card`/`Empty`/`Loading`/etc.) -- this
133
- * file adds layout/chrome only, no new business logic.
134
- *
135
- * Layout choices (documented, not the only valid ones):
136
- * - FILTER BAR (`data-slot="listing-layout-filter-bar"`): `sticky top-0`,
137
- * `border-b`/`bg-background`, and a `flex flex-wrap items-end gap-3` row
138
- * containing the injected `Search` (when `search` is passed),
139
- * `<ListingFilters>` (given the same horizontal row className plus a
140
- * `min-w-0` `groupClassName` so individual groups can shrink instead of
141
- * forcing overflow), and a trailing `ml-auto` cluster with
142
- * `<ListingResultHeader>` + `toolbarEnd`. `flex-wrap` is what makes this
143
- * gracefully reflow on narrow widths -- groups drop to new lines instead
144
- * of overflowing or requiring a separate mobile-only layout.
145
- * - SPLIT (`data-slot="listing-layout-split"`): a single CSS grid,
146
- * `md:grid-cols-[minmax(340px,42%)_1fr]` from `md` up (list column floors
147
- * at 340px, caps at 42% of the split's width; map takes the rest). Below
148
- * `md` there is no grid -- just two full-width panels, and exactly one is
149
- * visible at a time (see the mobile toggle below).
150
- * - LIST region (`data-slot="listing-layout-list"`): `overflow-y-auto`, its
151
- * own scroll container so browsing the list never requires scrolling the
152
- * map out of view. Holds `<ListingList>` (an auto-fill card grid) and
153
- * `<ListingPagination>`.
154
- * - MAP region (`data-slot="listing-layout-map"`): fills the split's full
155
- * height via the grid's default stretch alignment (no explicit height
156
- * needed), plus `md:sticky md:top-0` so that if a consuming app ever lets
157
- * `ListingLayout` sit inside a naturally document-scrolling page (rather
158
- * than the fixed-height shell this component defaults to via
159
- * `h-full min-h-0`), the map still pins in place while the list scrolls
160
- * past -- inert (but harmless) in the default fixed-height composition,
161
- * where there is nothing above the split to scroll past in the first
162
- * place. Renders `<ListingMap>` with a default centered fallback message
163
- * (`MAP_FALLBACK`) for when no `MapProvider` is configured, so the pane
164
- * never looks broken/blank.
165
- * - MOBILE LIST/MAP TOGGLE (`data-slot="listing-layout-mobile-toggle"`,
166
- * `md:hidden`): a small two-button segmented control, local `useState`
167
- * (`mobileView`), defaulting to `'list'`. Both the list and map regions
168
- * stay mounted at all times (never remounted on toggle -- that would
169
- * re-trigger `ListingList`'s/`ListingMap`'s own mount effects for no
170
- * reason); only their visibility flips via `hidden md:block` /
171
- * `block md:block`, i.e. the toggle only ever matters below `md` -- at
172
- * `md` and up both regions are always visible side by side and the toggle
173
- * control itself is hidden.
174
- * - Fetches the first page itself on mount (`engine.applyFilters({})`) by
175
- * default: none of the structure-only `~/react` compound components do
176
- * this (they are deliberately side-effect-free), so as the
177
- * batteries-included "full experience" entry point, `ListingLayout` is the
178
- * natural, single owner of that one bootstrapping side effect. Pass
179
- * `autoFetch={false}` to opt out and drive the first fetch yourself (e.g.
180
- * search-before-results, or when `urlSync` hydrates filters from the URL).
181
- */
182
- declare function ListingLayout({ search, className, toolbarEnd, autoFetch, mapCenter, mapZoom, }: IListingLayoutProps): react.JSX.Element;
183
-
184
- /**
185
- * `ListingApp.map` accepts either a ready-made `MapProvider` (any
186
- * implementation -- Google, a fake, a future provider) or, as a convenience,
187
- * a bare Google Maps API key. See `useResolvedMap`'s doc comment for why the
188
- * key shorthand is resolved via a dynamic `import()` rather than a static one.
189
- *
190
- * Both shapes optionally carry `center`/`zoom` -- the initial view, forwarded
191
- * through `ListingLayout`'s `mapCenter`/`mapZoom` straight into
192
- * `ListingMap`'s own `center`/`zoom` props. Omit them to get `ListingMap`'s
193
- * turnkey auto-fit default instead (see that component's doc comment);
194
- * supplying `center` opts out of auto-fit entirely.
195
- */
196
- type ListingAppMapProp = {
197
- provider: MapProvider;
198
- center?: LatLng;
199
- zoom?: number;
200
- } | {
201
- apiKey: string;
202
- mapId?: string;
203
- center?: LatLng;
204
- zoom?: number;
205
- };
206
- interface ListingAppProps<TFilters> {
207
- /**
208
- * One or more marker-layer datasets; the FIRST entry is the primary
209
- * dataset (drives the results list + pagination) -- same "insertion order"
210
- * rule `composeListingProviders`/`withDataset` already follow. Entity-erased
211
- * (`any`, not `TEntity`) for the same reason `DatasetRegistry<unknown,
212
- * TFilters>` is: one array can legitimately hold heterogeneous layers (a
213
- * properties dataset and a businesses dataset have different entity types)
214
- * -- see `compose-listing-providers.ts`'s `withDataset` doc comment.
215
- */
216
- datasets: DatasetDefinition<any, TFilters>[];
217
- /** A `(reg) => { reg.add(...); }` callback that registers your filters -- forwarded verbatim to `withFilters`. */
218
- filters?: (reg: FilterRegistry<TFilters>) => void;
219
- /** A ready `MapProvider`, or `{ apiKey, mapId? }` to build a `googleProvider` internally. Omit for no map (the styled layout shows a "Map unavailable" fallback). */
220
- map?: ListingAppMapProp;
221
- /** Slot overrides, merged OVER the shadcn styled defaults -- see this file's doc comment for how the merge works. */
222
- components?: Partial<IListingComponents>;
223
- /** A `UrlSyncController<TFilters>`, or `true`. `true` is a documented no-op: the library is filters-shape-erased and cannot generically derive a `TFilters <-> QueryParams` mapping, so build a real controller for your own filter shape. */
224
- urlSync?: UrlSyncController<TFilters> | boolean;
225
- initialFilters?: TFilters;
226
- config?: Partial<IListingConfigOptions>;
227
- className?: string;
228
- search?: IListingLayoutProps['search'];
229
- autoFetch?: boolean;
230
- }
231
- /**
232
- * Turnkey, batteries-included entry point: pass datasets + filters + a map +
233
- * component overrides + URL sync, and `ListingApp` composes
234
- * `composeListingProviders(...)`, `<ListingProvider>`, the styled `/shadcn`
235
- * defaults, and `<ListingLayout>` for you. Equivalent to (and internally
236
- * built from) the lower-level primitives every other `/shadcn` example wires
237
- * by hand -- reach for those directly when you need something `ListingApp`
238
- * doesn't expose (e.g. multiple `<ListingLayout>`s sharing one engine).
239
- *
240
- * `TEntity` still can't be inferred from these props (`datasets` narrows it,
241
- * but a multi-dataset array widens back to the union/`unknown` in practice)
242
- * -- annotate the call site when entity-level typing matters, exactly like
243
- * `<ListingProvider<TEntity, TFilters>>` itself.
244
- *
245
- * COMPONENTS MERGE: `components` overrides are applied ON TOP of the shadcn
246
- * styled defaults via ONE explicit `{ ...shadcnDefaultComponents, ...components }`
247
- * object, passed to a single `<ListingComponentsProvider>` -- never as a
248
- * nested `<ListingComponentsProvider>` inside `<ListingComponentsProviderWithDefaults>`.
249
- * `ListingComponentsProvider` merges `provided ?? ITS OWN private, unstyled
250
- * fallbacks` per slot (it does not read the parent context -- see
251
- * `src/react/components-provider.tsx`), so nesting would silently discard
252
- * every un-overridden shadcn default instead of keeping it.
253
- */
254
- declare function ListingApp<TEntity, TFilters>(props: ListingAppProps<TFilters>): react.JSX.Element | null;
255
-
256
- /** Merges class-name fragments, letting `tailwind-merge` resolve conflicting Tailwind utilities (last one wins). Mirrors `react-wizard-engine`'s `~/utils/cn`. */
257
- declare function cn(...inputs: ClassValue[]): string;
258
-
259
- export { DefaultCard, DefaultEmpty, DefaultFilterPanel, DefaultLoading, DefaultMarker, DefaultPopup, DefaultResultHeader, DefaultSearch, DefaultSidebar, DefaultToolbar, type IListingLayoutProps, ListingApp, type ListingAppMapProp, type ListingAppProps, ListingComponentsProviderWithDefaults, ListingLayout, cn, shadcnDefaultComponents };
@@ -1,2 +0,0 @@
1
- "use client";
2
- import{a as K}from"../chunk-35KYEBAC.js";import{A as X,B as Y,h as U,i as H,j as _,k as O,l as W,m as z,n as B,p as v,q as $,s as Z,v as G,x as q,y as J,z as Q}from"../chunk-R3FX2V3N.js";import{clsx as ie}from"clsx";import{twMerge as se}from"tailwind-merge";function s(...e){return se(ie(e))}import{Fragment as ne,jsx as m,jsxs as N}from"react/jsx-runtime";function ae(e){return typeof e!="number"?e:new Intl.NumberFormat("en-US",{style:"currency",currency:"USD",maximumFractionDigits:0}).format(e)}function w({item:e,selected:o,onSelect:t}){let r=e??{},l=s("flex w-full flex-col overflow-hidden rounded-lg border bg-card text-left text-card-foreground shadow-sm","motion-safe:transition-shadow",t&&"hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",o&&"ring-2 ring-ring"),a=N(ne,{children:[r.imageUrl&&m("img",{src:r.imageUrl,alt:r.title??"",className:"aspect-video w-full rounded-t-lg object-cover"}),N("div",{className:"flex flex-1 flex-col gap-1 p-3",children:[N("div",{className:"flex items-start justify-between gap-2",children:[r.title&&m("span",{className:"font-medium",children:r.title}),r.badge&&m("span",{className:"shrink-0 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground",children:r.badge})]}),r.subtitle&&m("span",{className:"text-sm text-muted-foreground",children:r.subtitle}),r.price!=null&&m("span",{className:"mt-1 font-semibold tabular-nums",children:ae(r.price)})]})]});return t?m("button",{type:"button",onClick:t,"aria-pressed":o??!1,className:l,children:a}):m("article",{className:l,children:a})}import{jsx as x,jsxs as j}from"react/jsx-runtime";function C(e){return j("div",{role:"status",className:"flex flex-col items-center justify-center gap-2 px-4 py-12 text-center text-muted-foreground",children:[j("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round",className:"h-8 w-8","aria-hidden":"true",children:[x("circle",{cx:"11",cy:"11",r:"7"}),x("path",{d:"m21 21-4.3-4.3"})]}),x("p",{className:"text-sm font-medium text-foreground",children:"No results"}),x("p",{className:"text-xs text-muted-foreground",children:"Try adjusting your filters or search terms."})]})}import{jsx as le}from"react/jsx-runtime";function I({children:e}){return le("div",{className:"flex flex-col gap-3",children:e})}import{jsx as L,jsxs as pe}from"react/jsx-runtime";function D(e){return L("div",{className:"flex flex-col gap-3 p-3",role:"status","aria-busy":"true","aria-label":"Loading results",children:Array.from({length:3}).map((o,t)=>pe("div",{className:"flex flex-col gap-2 motion-safe:animate-pulse",children:[L("div",{className:"aspect-video w-full rounded-md bg-muted"}),L("div",{className:"h-4 w-2/3 rounded-md bg-muted"}),L("div",{className:"h-3 w-1/3 rounded-md bg-muted"})]},t))})}import{jsx as de}from"react/jsx-runtime";function M({point:e}){let o=e.entity??{};return de("span",{className:"rounded-full border bg-background px-2 py-0.5 text-xs font-medium tabular-nums shadow-sm",children:o.price??""})}import{jsx as c,jsxs as k}from"react/jsx-runtime";function F({entity:e,onClose:o}){let t=e??{};return k("div",{className:"relative w-64 rounded-lg border bg-popover p-3 text-popover-foreground shadow-md",role:"group","aria-label":"Location details",children:[c("button",{type:"button",onClick:o,"aria-label":"Close",className:"absolute right-2 top-2 rounded-md p-1 text-muted-foreground motion-safe:transition-colors hover:bg-foreground/[0.06] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",children:k("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round",className:"h-4 w-4","aria-hidden":"true",children:[c("path",{d:"M18 6 6 18"}),c("path",{d:"m6 6 12 12"})]})}),k("div",{className:"pr-6",children:[t.title&&c("div",{className:"font-medium",children:t.title}),t.subtitle&&c("div",{className:"text-sm text-muted-foreground",children:t.subtitle}),t.price!=null&&c("div",{className:"mt-1 font-semibold tabular-nums",children:t.price})]})]})}import{jsx as me}from"react/jsx-runtime";function S({count:e,total:o}){let t=o!=null&&o!==e?`${e} of ${o} results`:`${e} results`;return me("div",{className:"text-sm tabular-nums text-muted-foreground",children:t})}import{jsx as ue}from"react/jsx-runtime";function T({value:e,onChange:o,placeholder:t}){return ue("input",{type:"search",value:e,placeholder:t,onChange:r=>o(r.target.value),"aria-label":t??"Search",className:s("h-9 w-full rounded-md border bg-background px-3 text-sm text-foreground placeholder:text-muted-foreground","focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring")})}import{jsx as fe}from"react/jsx-runtime";function R({children:e}){return fe("aside",{className:"flex w-full flex-col gap-4 p-4 md:border-r md:border-border",children:e})}import{jsx as ce}from"react/jsx-runtime";function A({children:e}){return ce("div",{className:"flex items-center gap-2 border-b p-2",children:e})}var g={Card:w,Marker:M,Popup:F,Sidebar:R,FilterPanel:I,Search:T,Empty:C,Loading:D,ResultHeader:S,Toolbar:A};import{jsx as be}from"react/jsx-runtime";function ge({children:e}){return be(v,{...g,children:e})}import{useEffect as ye,useState as he}from"react";import{useEffect as ve,useState as xe}from"react";import{jsx as i,jsxs as b}from"react/jsx-runtime";var Le=i("p",{className:"px-4 text-center text-sm text-muted-foreground",children:"Map unavailable - provide a Google Maps API key"});function E({search:e,className:o,toolbarEnd:t,autoFetch:r=!0,mapCenter:l,mapZoom:a}){let p=Z(),{Search:u}=$(),[f,y]=xe("list");ve(()=>{r!==!1&&p.applyFilters({})},[p,r]);let h=f==="map"?"hidden md:block":"block",P=f==="list"?"hidden md:block":"block";return b("div",{className:s("flex h-full min-h-0 w-full flex-col bg-background text-foreground",o),"data-slot":"listing-layout",children:[b("div",{className:"sticky top-0 z-10 flex flex-wrap items-end gap-3 border-b border-border bg-background p-3","data-slot":"listing-layout-filter-bar",children:[e&&i("div",{className:"w-full min-w-0 sm:w-auto sm:max-w-xs sm:flex-1",children:i(u,{value:e.value,onChange:e.onChange,placeholder:e.placeholder})}),i(G,{className:"flex flex-wrap items-end gap-3",groupClassName:"min-w-0"}),b("div",{className:"ml-auto flex items-center gap-3",children:[i(X,{}),t]})]}),i("div",{className:"flex items-center justify-center gap-1 border-b border-border bg-background p-2 md:hidden","data-slot":"listing-layout-mobile-toggle",children:i("div",{role:"group","aria-label":"View",className:"inline-flex rounded-md border border-border p-0.5",children:[{view:"list",label:"List"},{view:"map",label:"Map"}].map(({view:d,label:n})=>i("button",{type:"button","aria-pressed":f===d,onClick:()=>y(d),className:s("rounded-[5px] px-3 py-1 text-sm font-medium motion-safe:transition-colors",f===d?"bg-primary text-primary-foreground":"text-muted-foreground hover:bg-foreground/[0.06] hover:text-foreground"),children:n},d))})}),b("div",{className:"grid min-h-0 flex-1 md:grid-cols-[minmax(340px,42%)_1fr]","data-slot":"listing-layout-split",children:[b("div",{className:s("min-h-0 overflow-y-auto p-3",h),"data-slot":"listing-layout-list",children:[i(q,{className:"grid content-start gap-3 [grid-template-columns:repeat(auto-fill,minmax(200px,1fr))]"}),i("div",{className:"mt-4 flex justify-center",children:i(Q,{})})]}),i("div",{className:s("min-h-0 md:sticky md:top-0",P),"data-slot":"listing-layout-map",children:i(J,{center:l,zoom:a,fallback:Le})})]})]})}import{jsx as V}from"react/jsx-runtime";function ee(e){return"provider"in e}function Pe(e){let o=e!=null&&!ee(e),t=o?e.apiKey:void 0,r=o?e.mapId:void 0,[l,a]=he(()=>!e||ee(e)?{ready:!0,provider:e?.provider}:{ready:!1});return ye(()=>{if(!t)return;let p=!1;return import("../maps/google/index.js").then(({googleProvider:u})=>{p||a({ready:!0,provider:u({apiKey:t,mapId:r})})}),()=>{p=!0}},[t,r]),l}function Ne(e){let{datasets:o,filters:t,map:r,components:l,urlSync:a,initialFilters:p,config:u,className:f,search:y,autoFetch:h}=e,{ready:P,provider:d}=Pe(r);if(!P)return null;let n=[];for(let oe of o)n.push(O(oe));t&&n.push(W(t)),d&&n.push(_(d)),a instanceof K&&n.push(z(a)),p&&n.push(B(p)),u&&n.push(H(u));let te=U(...n),re={...g,...l};return V(Y,{...te,children:V(v,{...re,children:V(E,{className:f,search:y,autoFetch:h,mapCenter:r?.center,mapZoom:r?.zoom})})})}export{w as DefaultCard,C as DefaultEmpty,I as DefaultFilterPanel,D as DefaultLoading,M as DefaultMarker,F as DefaultPopup,S as DefaultResultHeader,T as DefaultSearch,R as DefaultSidebar,A as DefaultToolbar,Ne as ListingApp,ge as ListingComponentsProviderWithDefaults,E as ListingLayout,s as cn,g as shadcnDefaultComponents};
@@ -1,89 +0,0 @@
1
- import { Q as QueryParams } from './map-provider.interface-DT-v1plm.js';
2
-
3
- /**
4
- * DOM-agnostic abstraction over "the URL's query string" — or, in tests/SSR,
5
- * an in-memory stand-in (`MemoryHistoryPort`). `UrlSyncController` talks to
6
- * this interface only, never to `window.location`/`history` directly, so it
7
- * stays framework- and environment-free like the rest of `src/core`.
8
- */
9
- interface HistoryPort {
10
- getQuery(): QueryParams;
11
- setQuery(params: QueryParams): void;
12
- subscribe(cb: () => void): () => void;
13
- }
14
-
15
- interface UrlSyncEngine<TFilters> {
16
- subscribe(cb: () => void): () => void;
17
- readonly state: {
18
- readonly filters: TFilters;
19
- };
20
- applyFilters(patch: Partial<TFilters>): Promise<void> | void;
21
- }
22
- interface UrlSyncOptions<TFilters> {
23
- history: HistoryPort;
24
- toQuery(filters: TFilters): QueryParams;
25
- toFilters(query: QueryParams): Partial<TFilters>;
26
- hydrateOnStart?: boolean;
27
- }
28
- /**
29
- * Bidirectional, DOM-agnostic sync between an engine's `filters` and a
30
- * `HistoryPort`'s query params. Framework-free: talks to `UrlSyncEngine`
31
- * (a structural subset `ListingEngine` satisfies) and `HistoryPort`, nothing
32
- * else — no `window`, no React.
33
- *
34
- * OPTIONAL, not the primary path: `styled/listing-app.tsx`'s turnkey
35
- * `ListingApp` (the package's main-entry default) does NOT wire this up —
36
- * it uses an event-based URL API instead (`initialFilters` in,
37
- * `onFiltersChange` out; see that file's doc comment), so the library never
38
- * touches `window.history` by default. This class remains exported for
39
- * consumers who explicitly want the library to own history writes/reads
40
- * itself (e.g. via `/shadcn`'s `ListingApp.urlSync`, or wiring it directly
41
- * against a hand-built engine).
42
- *
43
- * Echo-loop guard: both subscriptions below are driven by the SAME
44
- * `isSyncing` flag. `ListingEngine.applyFilters()` and `MemoryHistoryPort`
45
- * (and any real browser HistoryPort) both notify their subscribers
46
- * SYNCHRONOUSLY as part of the write (`store.setFilters()` -> `notify()`
47
- * happens before any debounce timer, and `MemoryHistoryPort.setQuery()`
48
- * notifies before returning) — so when this controller initiates a write on
49
- * one side, the reciprocal subscription on the other side fires within the
50
- * very same call stack, before `isSyncing` is reset. Wrapping each
51
- * controller-initiated write in `isSyncing = true; ...; isSyncing = false`
52
- * (via try/finally, so a throwing `toQuery`/`toFilters`/notify can't leave it
53
- * stuck) means that reciprocal callback observes `isSyncing === true` and
54
- * short-circuits instead of writing back — one hop each direction, never a
55
- * cascade. `hydrateOnStart`'s initial engine write is wrapped the same way,
56
- * since it must not immediately echo the just-hydrated filters back out to
57
- * history as a redundant `setQuery`.
58
- *
59
- * `isSyncing` only covers that SYNCHRONOUS window, though — the real
60
- * `ListingEngine.applyFilters()` notifies AGAIN, asynchronously: after
61
- * `await adapter.list(...)` resolves, `store.setResults()` and
62
- * `store.setLoading(false)` each call `notify()`, well after `isSyncing` has
63
- * already been reset to `false` by the `withSyncGuard` that kicked the query
64
- * off. Left unguarded, that async tail would fire the engine subscription
65
- * below and echo an identical (but spurious) `history.setQuery(...)` on
66
- * every completed query. Rather than widening `isSyncing` to cover the whole
67
- * async query (which would also swallow legitimate concurrent history
68
- * changes that land mid-query), `syncEngineToHistory` is idempotent BY
69
- * VALUE instead: it computes the target query and skips the write entirely
70
- * when it already matches `history.getQuery()` — timing-independent, so it
71
- * suppresses the async-tail echo without touching `isSyncing` at all.
72
- */
73
- declare class UrlSyncController<TFilters> {
74
- private readonly history;
75
- private readonly toQueryFn;
76
- private readonly toFiltersFn;
77
- private readonly hydrateOnStart;
78
- private isSyncing;
79
- private unsubscribeEngine;
80
- private unsubscribeHistory;
81
- constructor(opts: UrlSyncOptions<TFilters>);
82
- start(engine: UrlSyncEngine<TFilters>): void;
83
- stop(): void;
84
- private syncEngineToHistory;
85
- private applyFiltersSafely;
86
- private withSyncGuard;
87
- }
88
-
89
- export { type HistoryPort as H, UrlSyncController as U, type UrlSyncEngine as a, type UrlSyncOptions as b };