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
@@ -0,0 +1,972 @@
1
+ /*!
2
+ * react-listing-engine / styled -- Tailwind-free CSS layer.
3
+ *
4
+ * THEMING API
5
+ * -----------
6
+ * This is the ONLY stylesheet the `/styled` adapter ships. Every visual
7
+ * value below (color, radius, shadow, spacing, font size) is read from a
8
+ * `--rle-*` custom property declared on `:root`. Retheme every `.rle-*`
9
+ * component at once by overriding a subset of them in your OWN CSS, loaded
10
+ * after this file -- no build step, no Tailwind config, no JS:
11
+ *
12
+ * import 'react-listing-engine/styles.css';
13
+ * import './my-theme.css'; // :root { --rle-primary: #0ea5e9; --rle-radius: 4px; }
14
+ *
15
+ * Full variable list (defaults shown = light theme, matches the TenantCloud
16
+ * /find look):
17
+ *
18
+ * --rle-primary accent color (buttons, marker pills, focus ring default)
19
+ * --rle-primary-contrast text color on top of --rle-primary
20
+ * --rle-bg page-level background (the `.rle-app` layout shell;
21
+ * individual components use --rle-surface instead)
22
+ * --rle-surface card/popup/control background
23
+ * --rle-fg primary text color
24
+ * --rle-muted secondary/muted text color
25
+ * --rle-muted-bg muted fill (skeletons, subtle badges, hover washes)
26
+ * --rle-border hairline border color
27
+ * --rle-ring focus-visible / selected ring color
28
+ * --rle-danger destructive/error state color
29
+ * --rle-radius large radius (cards, popups, dropdown panels)
30
+ * --rle-radius-sm small radius (inputs, buttons, skeletons)
31
+ * --rle-radius-pill fully-rounded (marker pill, badges)
32
+ * --rle-shadow resting/elevated shadow (popups, hover cards, dropdown panels)
33
+ * --rle-shadow-sm subtle shadow (marker pill, resting card)
34
+ * --rle-space base spacing unit
35
+ * --rle-font-sans font stack for every `.rle-*` element (default: inherit page font)
36
+ * --rle-fs-sm small text size (captions, periods, hints)
37
+ * --rle-fs-md body text size (labels, addresses, inputs)
38
+ * --rle-fs-lg large text size (card/popup titles)
39
+ *
40
+ * SCOPING
41
+ * -------
42
+ * Every rule in this file is anchored to a `.rle-` class -- nothing here
43
+ * targets bare tags (`div`, `button`, ...) or `*`, so importing this
44
+ * stylesheet can never bleed onto a consuming app's own markup, and the
45
+ * app's own CSS can never accidentally reach in and restyle these
46
+ * components except through the documented `--rle-*` variables.
47
+ */
48
+
49
+ :root {
50
+ --rle-primary: #41a541;
51
+ --rle-primary-contrast: #ffffff;
52
+ --rle-bg: #ffffff;
53
+ --rle-surface: #ffffff;
54
+ --rle-fg: #334e68;
55
+ --rle-muted: #627d98;
56
+ --rle-muted-bg: #f0f4f8;
57
+ --rle-border: #d9e2ec;
58
+ --rle-ring: #41a541;
59
+ --rle-danger: #ba2525;
60
+ --rle-radius: 10px;
61
+ --rle-radius-sm: 6px;
62
+ --rle-radius-pill: 999px;
63
+ --rle-shadow: 0 0 32px rgba(136, 152, 170, 0.15);
64
+ --rle-shadow-sm: 0 1px 3px rgba(50, 50, 93, 0.12);
65
+ --rle-space: 12px;
66
+ --rle-font-sans: inherit;
67
+ --rle-fs-sm: 12px;
68
+ --rle-fs-md: 14px;
69
+ --rle-fs-lg: 16px;
70
+ }
71
+
72
+ /* -----------------------------------------------------------------------
73
+ * Base -- box-sizing + font-family for every element this stylesheet owns.
74
+ * --------------------------------------------------------------------- */
75
+
76
+ .rle-card,
77
+ .rle-card-media,
78
+ .rle-card-body,
79
+ .rle-marker,
80
+ .rle-pin,
81
+ .rle-popup,
82
+ .rle-popup-close,
83
+ .rle-empty,
84
+ .rle-empty-icon,
85
+ .rle-loading,
86
+ .rle-loading-item,
87
+ .rle-skeleton,
88
+ .rle-result-header,
89
+ .rle-toolbar,
90
+ .rle-sidebar,
91
+ .rle-filter-panel,
92
+ .rle-input,
93
+ .rle-range,
94
+ .rle-select,
95
+ .rle-dropdown,
96
+ .rle-dropdown-panel,
97
+ .rle-checkbox,
98
+ .rle-label,
99
+ .rle-btn {
100
+ box-sizing: border-box;
101
+ font-family: var(--rle-font-sans);
102
+ }
103
+
104
+ /* -----------------------------------------------------------------------
105
+ * Card
106
+ * --------------------------------------------------------------------- */
107
+
108
+ .rle-card {
109
+ display: flex;
110
+ width: 100%;
111
+ flex-direction: column;
112
+ overflow: hidden;
113
+ background: var(--rle-surface);
114
+ border: 1px solid var(--rle-border);
115
+ border-radius: var(--rle-radius);
116
+ box-shadow: var(--rle-shadow-sm);
117
+ color: var(--rle-fg);
118
+ text-align: left;
119
+ font: inherit;
120
+ }
121
+
122
+ /* Only the interactive (`onSelect` given) variant renders as a real
123
+ `<button type="button" class="rle-card">` -- reset browser button chrome
124
+ and add the pointer/hover/focus affordances only there, so the
125
+ non-interactive `<article class="rle-card">` variant stays inert. */
126
+ .rle-card[type='button'] {
127
+ margin: 0;
128
+ padding: 0;
129
+ background: var(--rle-surface);
130
+ border: 1px solid var(--rle-border);
131
+ cursor: pointer;
132
+ transition:
133
+ box-shadow 150ms ease,
134
+ border-color 150ms ease;
135
+ }
136
+
137
+ .rle-card[type='button']:hover {
138
+ box-shadow: var(--rle-shadow);
139
+ }
140
+
141
+ .rle-card[type='button']:focus-visible {
142
+ outline: none;
143
+ border-color: var(--rle-ring);
144
+ box-shadow: 0 0 0 3px var(--rle-ring);
145
+ }
146
+
147
+ .rle-card--selected {
148
+ border-color: var(--rle-ring);
149
+ box-shadow: 0 0 0 2px var(--rle-ring);
150
+ }
151
+
152
+ .rle-card-media {
153
+ display: block;
154
+ width: 100%;
155
+ aspect-ratio: 4 / 3;
156
+ object-fit: cover;
157
+ background: var(--rle-muted-bg);
158
+ border-top-left-radius: var(--rle-radius);
159
+ border-top-right-radius: var(--rle-radius);
160
+ }
161
+
162
+ .rle-card-media--placeholder {
163
+ background: var(--rle-muted-bg);
164
+ }
165
+
166
+ .rle-card-body {
167
+ display: flex;
168
+ flex: 1;
169
+ flex-direction: column;
170
+ gap: 4px;
171
+ padding: var(--rle-space);
172
+ }
173
+
174
+ .rle-card-title {
175
+ font-size: var(--rle-fs-lg);
176
+ font-weight: 600;
177
+ color: var(--rle-fg);
178
+ line-height: 1.3;
179
+ }
180
+
181
+ .rle-card-address {
182
+ font-size: var(--rle-fs-md);
183
+ color: var(--rle-muted);
184
+ }
185
+
186
+ .rle-card-info {
187
+ display: flex;
188
+ flex-wrap: wrap;
189
+ align-items: center;
190
+ font-size: var(--rle-fs-md);
191
+ color: var(--rle-muted);
192
+ }
193
+
194
+ .rle-card-info-item:not(:first-child)::before {
195
+ content: '\00b7';
196
+ display: inline-block;
197
+ margin: 0 0.4em;
198
+ color: var(--rle-muted);
199
+ }
200
+
201
+ .rle-card-price {
202
+ margin-top: 2px;
203
+ font-size: var(--rle-fs-lg);
204
+ font-weight: 700;
205
+ color: var(--rle-fg);
206
+ font-variant-numeric: tabular-nums;
207
+ }
208
+
209
+ .rle-card-price-period {
210
+ margin-left: 0.25em;
211
+ font-size: var(--rle-fs-sm);
212
+ font-weight: 400;
213
+ color: var(--rle-muted);
214
+ }
215
+
216
+ /* -----------------------------------------------------------------------
217
+ * Marker pill
218
+ * --------------------------------------------------------------------- */
219
+
220
+ .rle-marker {
221
+ display: inline-flex;
222
+ align-items: center;
223
+ white-space: nowrap;
224
+ padding: 4px 10px;
225
+ background: var(--rle-primary);
226
+ color: var(--rle-primary-contrast);
227
+ border-radius: var(--rle-radius-pill);
228
+ font-size: var(--rle-fs-sm);
229
+ font-weight: 600;
230
+ font-variant-numeric: tabular-nums;
231
+ box-shadow: var(--rle-shadow-sm);
232
+ }
233
+
234
+ /* -----------------------------------------------------------------------
235
+ * Map pin -- Rentler-style teardrop price marker: `.rle-marker` above is a
236
+ * plain pill; this is the same pill PLUS a pointed tail (`::after`, a
237
+ * rotated square) so the marker's visual tip lands on the exact
238
+ * coordinate -- matches `AdvancedMarkerElement`'s default content anchor
239
+ * (bottom-center). `properties-dataset.ts`'s `defaultPriceMarkerElement` is
240
+ * the raw-DOM (non-CSS-class) equivalent used for the map's own `content`
241
+ * node, built with the same shape via inline styles.
242
+ * --------------------------------------------------------------------- */
243
+
244
+ .rle-pin {
245
+ position: relative;
246
+ display: inline-flex;
247
+ align-items: center;
248
+ white-space: nowrap;
249
+ padding: 4px 9px;
250
+ background: var(--rle-primary);
251
+ color: var(--rle-primary-contrast);
252
+ border-radius: var(--rle-radius-pill);
253
+ font-size: var(--rle-fs-sm);
254
+ font-weight: 700;
255
+ font-variant-numeric: tabular-nums;
256
+ box-shadow: var(--rle-shadow-sm);
257
+ }
258
+
259
+ .rle-pin::after {
260
+ content: '';
261
+ position: absolute;
262
+ bottom: -3px;
263
+ left: 50%;
264
+ width: 8px;
265
+ height: 8px;
266
+ background: inherit;
267
+ border-radius: 0 0 2px 0;
268
+ /* `background: inherit` (not a literal color) so `.rle-pin--selected`'s
269
+ darker background is picked up by the pointer automatically -- no
270
+ separate selected rule needed for this pseudo-element. */
271
+ transform: translateX(-50%) rotate(45deg);
272
+ }
273
+
274
+ .rle-pin--selected {
275
+ filter: brightness(0.82);
276
+ box-shadow: 0 0 0 2px var(--rle-ring);
277
+ }
278
+
279
+ /* -----------------------------------------------------------------------
280
+ * Popup
281
+ * --------------------------------------------------------------------- */
282
+
283
+ .rle-popup {
284
+ position: relative;
285
+ max-width: 18rem;
286
+ padding: var(--rle-space);
287
+ background: var(--rle-surface);
288
+ border: 1px solid var(--rle-border);
289
+ border-radius: var(--rle-radius);
290
+ box-shadow: var(--rle-shadow);
291
+ color: var(--rle-fg);
292
+ }
293
+
294
+ .rle-popup-close {
295
+ position: absolute;
296
+ top: 8px;
297
+ right: 8px;
298
+ display: inline-flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ width: 28px;
302
+ height: 28px;
303
+ padding: 0;
304
+ background: transparent;
305
+ border: none;
306
+ border-radius: var(--rle-radius-sm);
307
+ color: var(--rle-muted);
308
+ cursor: pointer;
309
+ transition:
310
+ background-color 150ms ease,
311
+ color 150ms ease;
312
+ }
313
+
314
+ .rle-popup-close:hover {
315
+ background: var(--rle-muted-bg);
316
+ color: var(--rle-fg);
317
+ }
318
+
319
+ .rle-popup-close:focus-visible {
320
+ outline: none;
321
+ box-shadow: 0 0 0 2px var(--rle-ring);
322
+ }
323
+
324
+ /* -----------------------------------------------------------------------
325
+ * States -- empty / loading / result header
326
+ * --------------------------------------------------------------------- */
327
+
328
+ .rle-empty {
329
+ display: flex;
330
+ flex-direction: column;
331
+ align-items: center;
332
+ justify-content: center;
333
+ gap: 8px;
334
+ padding: calc(var(--rle-space) * 3) var(--rle-space);
335
+ text-align: center;
336
+ color: var(--rle-muted);
337
+ }
338
+
339
+ .rle-empty-icon {
340
+ width: 32px;
341
+ height: 32px;
342
+ color: var(--rle-muted);
343
+ }
344
+
345
+ .rle-empty-title {
346
+ font-size: var(--rle-fs-md);
347
+ font-weight: 600;
348
+ color: var(--rle-fg);
349
+ }
350
+
351
+ .rle-empty-hint {
352
+ font-size: var(--rle-fs-sm);
353
+ color: var(--rle-muted);
354
+ }
355
+
356
+ .rle-loading {
357
+ display: grid;
358
+ gap: var(--rle-space);
359
+ padding: var(--rle-space);
360
+ }
361
+
362
+ .rle-loading-item {
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: 6px;
366
+ }
367
+
368
+ .rle-skeleton {
369
+ background: var(--rle-muted-bg);
370
+ border-radius: var(--rle-radius-sm);
371
+ animation: rle-pulse 1.5s ease-in-out infinite;
372
+ }
373
+
374
+ @keyframes rle-pulse {
375
+ 0%,
376
+ 100% {
377
+ opacity: 1;
378
+ }
379
+ 50% {
380
+ opacity: 0.45;
381
+ }
382
+ }
383
+
384
+ .rle-result-header {
385
+ font-size: var(--rle-fs-md);
386
+ color: var(--rle-muted);
387
+ font-variant-numeric: tabular-nums;
388
+ }
389
+
390
+ /* -----------------------------------------------------------------------
391
+ * Layout-adjacent chrome (Toolbar / Sidebar / FilterPanel) -- not part of
392
+ * the Phase 2 layout contract itself, just the default slot components'
393
+ * own chrome. Kept intentionally simple/non-responsive here; Phase 2 adds
394
+ * layout + mobile classes alongside these without needing to change them.
395
+ * --------------------------------------------------------------------- */
396
+
397
+ .rle-toolbar {
398
+ display: flex;
399
+ align-items: center;
400
+ gap: var(--rle-space);
401
+ padding: calc(var(--rle-space) * 0.75) var(--rle-space);
402
+ border-bottom: 1px solid var(--rle-border);
403
+ }
404
+
405
+ .rle-sidebar {
406
+ display: flex;
407
+ flex-direction: column;
408
+ gap: var(--rle-space);
409
+ padding: var(--rle-space);
410
+ }
411
+
412
+ .rle-filter-panel {
413
+ display: flex;
414
+ flex-direction: column;
415
+ gap: var(--rle-space);
416
+ }
417
+
418
+ /* -----------------------------------------------------------------------
419
+ * Form controls
420
+ * --------------------------------------------------------------------- */
421
+
422
+ .rle-label {
423
+ display: block;
424
+ margin-bottom: 0.35em;
425
+ font-size: var(--rle-fs-sm);
426
+ font-weight: 500;
427
+ color: var(--rle-fg);
428
+ }
429
+
430
+ .rle-input {
431
+ display: block;
432
+ width: 100%;
433
+ height: 36px;
434
+ padding: 0 0.75em;
435
+ background: var(--rle-surface);
436
+ border: 1px solid var(--rle-border);
437
+ border-radius: var(--rle-radius-sm);
438
+ color: var(--rle-fg);
439
+ font-size: var(--rle-fs-md);
440
+ transition:
441
+ border-color 150ms ease,
442
+ box-shadow 150ms ease;
443
+ }
444
+
445
+ .rle-input::placeholder {
446
+ color: var(--rle-muted);
447
+ }
448
+
449
+ .rle-input:focus-visible {
450
+ outline: none;
451
+ border-color: var(--rle-primary);
452
+ box-shadow: 0 0 0 3px var(--rle-ring);
453
+ }
454
+
455
+ .rle-range {
456
+ display: flex;
457
+ align-items: center;
458
+ gap: 8px;
459
+ }
460
+
461
+ .rle-range .rle-input {
462
+ min-width: 0;
463
+ flex: 1;
464
+ }
465
+
466
+ .rle-range-sep {
467
+ color: var(--rle-muted);
468
+ font-size: var(--rle-fs-md);
469
+ }
470
+
471
+ .rle-select,
472
+ .rle-dropdown {
473
+ position: relative;
474
+ display: inline-flex;
475
+ width: 100%;
476
+ height: 36px;
477
+ align-items: center;
478
+ justify-content: space-between;
479
+ gap: 8px;
480
+ padding: 0 0.75em;
481
+ background: var(--rle-surface);
482
+ border: 1px solid var(--rle-border);
483
+ border-radius: var(--rle-radius-sm);
484
+ color: var(--rle-fg);
485
+ font-size: var(--rle-fs-md);
486
+ cursor: pointer;
487
+ transition:
488
+ border-color 150ms ease,
489
+ box-shadow 150ms ease;
490
+ }
491
+
492
+ .rle-select:focus-visible,
493
+ .rle-dropdown:focus-visible {
494
+ outline: none;
495
+ border-color: var(--rle-primary);
496
+ box-shadow: 0 0 0 3px var(--rle-ring);
497
+ }
498
+
499
+ .rle-dropdown-panel {
500
+ position: absolute;
501
+ top: calc(100% + 4px);
502
+ left: 0;
503
+ z-index: 20;
504
+ min-width: 100%;
505
+ padding: 4px;
506
+ background: var(--rle-surface);
507
+ border: 1px solid var(--rle-border);
508
+ border-radius: var(--rle-radius-sm);
509
+ box-shadow: var(--rle-shadow);
510
+ }
511
+
512
+ .rle-checkbox {
513
+ width: 16px;
514
+ height: 16px;
515
+ accent-color: var(--rle-primary);
516
+ }
517
+
518
+ /* -----------------------------------------------------------------------
519
+ * Buttons
520
+ * --------------------------------------------------------------------- */
521
+
522
+ .rle-btn {
523
+ display: inline-flex;
524
+ align-items: center;
525
+ justify-content: center;
526
+ gap: 0.4em;
527
+ height: 36px;
528
+ padding: 0 1em;
529
+ background: var(--rle-surface);
530
+ border: 1px solid var(--rle-border);
531
+ border-radius: var(--rle-radius-sm);
532
+ color: var(--rle-fg);
533
+ font-size: var(--rle-fs-md);
534
+ font-weight: 500;
535
+ cursor: pointer;
536
+ transition:
537
+ background-color 150ms ease,
538
+ border-color 150ms ease,
539
+ box-shadow 150ms ease,
540
+ color 150ms ease;
541
+ }
542
+
543
+ .rle-btn:hover {
544
+ background: var(--rle-muted-bg);
545
+ }
546
+
547
+ .rle-btn:focus-visible {
548
+ outline: none;
549
+ border-color: var(--rle-primary);
550
+ box-shadow: 0 0 0 3px var(--rle-ring);
551
+ }
552
+
553
+ .rle-btn--primary {
554
+ background: var(--rle-primary);
555
+ border-color: var(--rle-primary);
556
+ color: var(--rle-primary-contrast);
557
+ }
558
+
559
+ .rle-btn--primary:hover {
560
+ filter: brightness(0.94);
561
+ }
562
+
563
+ .rle-btn--ghost {
564
+ background: transparent;
565
+ border-color: transparent;
566
+ }
567
+
568
+ .rle-btn--ghost:hover {
569
+ background: var(--rle-muted-bg);
570
+ }
571
+
572
+ .rle-btn--sm {
573
+ height: 28px;
574
+ padding: 0 0.75em;
575
+ border-radius: var(--rle-radius-sm);
576
+ font-size: var(--rle-fs-sm);
577
+ }
578
+
579
+ /* -----------------------------------------------------------------------
580
+ * Motion
581
+ * --------------------------------------------------------------------- */
582
+
583
+ @media (prefers-reduced-motion: reduce) {
584
+ .rle-card[type='button'],
585
+ .rle-popup-close,
586
+ .rle-skeleton,
587
+ .rle-input,
588
+ .rle-select,
589
+ .rle-dropdown,
590
+ .rle-btn,
591
+ .rle-bottom-nav__btn,
592
+ .rle-viewtoggle__btn,
593
+ .rle-sheet,
594
+ .rle-sheet-backdrop {
595
+ transition: none;
596
+ animation: none;
597
+ }
598
+ }
599
+
600
+ /* -----------------------------------------------------------------------
601
+ * Layout shell -- the responsive "filter bar + list/map split" chrome
602
+ * `StyledListingLayout` composes the `~/react` structure-only components
603
+ * into. Breakpoint: 768px, matching the mobile-only bottom nav/sheet below.
604
+ * Mobile-first: rules with no media query apply to every width; a
605
+ * `(min-width: 768px)` block layers desktop-only overrides, a
606
+ * `(max-width: 767px)` block layers mobile-only overrides -- never both for
607
+ * the same property, so there is exactly one source of truth per viewport.
608
+ * --------------------------------------------------------------------- */
609
+
610
+ .rle-app,
611
+ .rle-filter-bar,
612
+ .rle-filter-bar__end,
613
+ .rle-filters-row,
614
+ .rle-filter-stack,
615
+ .rle-filter-group,
616
+ .rle-body,
617
+ .rle-split,
618
+ .rle-list,
619
+ .rle-list-grid,
620
+ .rle-map,
621
+ .rle-bottom-nav,
622
+ .rle-bottom-nav__btn,
623
+ .rle-viewtoggle,
624
+ .rle-viewtoggle__btn,
625
+ .rle-sheet-backdrop,
626
+ .rle-sheet,
627
+ .rle-sheet__handle,
628
+ .rle-sheet__header,
629
+ .rle-sheet__title,
630
+ .rle-sheet__close,
631
+ .rle-sheet__body,
632
+ .rle-sheet__footer {
633
+ box-sizing: border-box;
634
+ font-family: var(--rle-font-sans);
635
+ }
636
+
637
+ .rle-app {
638
+ display: flex;
639
+ flex-direction: column;
640
+ height: 100%;
641
+ background: var(--rle-bg);
642
+ color: var(--rle-fg);
643
+ }
644
+
645
+ /* -----------------------------------------------------------------------
646
+ * Desktop filter bar -- hidden below 768px in favor of the bottom nav's
647
+ * Filters button + bottom sheet.
648
+ * --------------------------------------------------------------------- */
649
+
650
+ .rle-filter-bar {
651
+ position: sticky;
652
+ top: 0;
653
+ z-index: 10;
654
+ display: flex;
655
+ flex-wrap: wrap;
656
+ align-items: flex-end;
657
+ gap: var(--rle-space);
658
+ padding: var(--rle-space);
659
+ background: var(--rle-surface);
660
+ border-bottom: 1px solid var(--rle-border);
661
+ }
662
+
663
+ .rle-filter-bar__end {
664
+ display: flex;
665
+ align-items: center;
666
+ gap: var(--rle-space);
667
+ margin-left: auto;
668
+ }
669
+
670
+ @media (max-width: 767px) {
671
+ .rle-filter-bar {
672
+ display: none;
673
+ }
674
+ }
675
+
676
+ /* `<ListingFilters>`'s own `className`/`groupClassName` hooks (see that
677
+ component's doc comment in `~/react`) -- a horizontal, wrapping row for the
678
+ desktop filter bar, a vertical stack for the mobile sheet body, and a
679
+ shrinkable wrapper for each individual filter group in either layout. */
680
+ .rle-filters-row {
681
+ display: flex;
682
+ flex-wrap: wrap;
683
+ align-items: flex-end;
684
+ gap: var(--rle-space);
685
+ }
686
+
687
+ .rle-filter-stack {
688
+ display: flex;
689
+ flex-direction: column;
690
+ gap: var(--rle-space);
691
+ }
692
+
693
+ .rle-filter-group {
694
+ min-width: 0;
695
+ }
696
+
697
+ /* -----------------------------------------------------------------------
698
+ * Body / split -- list-left, map-right from 768px up; a single full-area
699
+ * panel (one of `.rle-list`/`.rle-map` visible at a time) below it.
700
+ * --------------------------------------------------------------------- */
701
+
702
+ .rle-body {
703
+ position: relative;
704
+ flex: 1;
705
+ min-height: 0;
706
+ }
707
+
708
+ .rle-split {
709
+ display: grid;
710
+ min-height: 0;
711
+ height: 100%;
712
+ }
713
+
714
+ @media (min-width: 768px) {
715
+ .rle-split {
716
+ grid-template-columns: minmax(340px, 42%) 1fr;
717
+ }
718
+ }
719
+
720
+ .rle-list {
721
+ min-height: 0;
722
+ height: 100%;
723
+ overflow-y: auto;
724
+ padding: var(--rle-space);
725
+ }
726
+
727
+ .rle-list-grid {
728
+ display: grid;
729
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
730
+ gap: var(--rle-space);
731
+ }
732
+
733
+ .rle-map {
734
+ min-height: 0;
735
+ height: 100%;
736
+ }
737
+
738
+ @media (max-width: 767px) {
739
+ /* Exactly one of list/map visible at a time, driven by the mobile-view
740
+ toggle in the bottom nav -- see `listing-layout.tsx`. Scoped to mobile
741
+ only: at 768px+ `.rle-split`'s two grid columns keep both visible
742
+ regardless of this data attribute's current value. */
743
+ .rle-body[data-mobile-view='list'] .rle-map {
744
+ display: none;
745
+ }
746
+
747
+ .rle-body[data-mobile-view='map'] .rle-list {
748
+ display: none;
749
+ }
750
+
751
+ /* Reserve room for the floating bottom-nav pill so the last card/control
752
+ isn't hidden behind it (see `.rle-bottom-nav` below -- its own height
753
+ plus its floating offset from the viewport bottom, ~72px). */
754
+ .rle-list {
755
+ padding-bottom: calc(72px + env(safe-area-inset-bottom));
756
+ }
757
+ }
758
+
759
+ /* -----------------------------------------------------------------------
760
+ * Bottom nav -- mobile only (Filters / optional action / List|Map toggle).
761
+ * A centered, floating rounded PILL, not a full-width bar (Rentler mobile
762
+ * reference): `left: 50%` + `transform: translateX(-50%)` centers it,
763
+ * `width: auto` + `max-width` lets it shrink-to-fit its buttons instead of
764
+ * spanning the viewport, and `bottom` floats it clear of the safe area
765
+ * instead of sitting flush against the edge.
766
+ * --------------------------------------------------------------------- */
767
+
768
+ .rle-bottom-nav {
769
+ display: none;
770
+ }
771
+
772
+ @media (max-width: 767px) {
773
+ .rle-bottom-nav {
774
+ position: fixed;
775
+ left: 50%;
776
+ bottom: calc(12px + env(safe-area-inset-bottom));
777
+ z-index: 30;
778
+ display: flex;
779
+ align-items: stretch;
780
+ width: auto;
781
+ max-width: calc(100% - 24px);
782
+ gap: 4px;
783
+ padding: 6px 8px;
784
+ background: var(--rle-surface);
785
+ border: 1px solid var(--rle-border);
786
+ border-radius: var(--rle-radius-pill);
787
+ box-shadow: var(--rle-shadow);
788
+ transform: translateX(-50%);
789
+ }
790
+ }
791
+
792
+ .rle-bottom-nav__btn {
793
+ display: flex;
794
+ flex: 1;
795
+ flex-direction: column;
796
+ align-items: center;
797
+ justify-content: center;
798
+ gap: 2px;
799
+ min-height: 44px;
800
+ padding: 6px 4px;
801
+ background: transparent;
802
+ border: none;
803
+ color: var(--rle-muted);
804
+ font-size: var(--rle-fs-sm);
805
+ font-weight: 500;
806
+ line-height: 1.2;
807
+ cursor: pointer;
808
+ transition:
809
+ color 150ms ease,
810
+ background-color 150ms ease;
811
+ }
812
+
813
+ .rle-bottom-nav__btn svg {
814
+ width: 20px;
815
+ height: 20px;
816
+ }
817
+
818
+ .rle-bottom-nav__btn:hover {
819
+ background: var(--rle-muted-bg);
820
+ }
821
+
822
+ .rle-bottom-nav__btn:focus-visible {
823
+ outline: none;
824
+ box-shadow: inset 0 0 0 2px var(--rle-ring);
825
+ }
826
+
827
+ .rle-bottom-nav__btn--active {
828
+ color: var(--rle-primary);
829
+ }
830
+
831
+ .rle-viewtoggle {
832
+ display: flex;
833
+ flex: 1;
834
+ align-items: center;
835
+ justify-content: center;
836
+ gap: 2px;
837
+ padding: 6px;
838
+ }
839
+
840
+ .rle-viewtoggle__btn {
841
+ display: inline-flex;
842
+ min-height: 32px;
843
+ align-items: center;
844
+ justify-content: center;
845
+ gap: 4px;
846
+ padding: 4px 12px;
847
+ background: transparent;
848
+ border: none;
849
+ border-radius: var(--rle-radius-pill);
850
+ color: var(--rle-muted);
851
+ font-size: var(--rle-fs-sm);
852
+ font-weight: 500;
853
+ cursor: pointer;
854
+ transition:
855
+ background-color 150ms ease,
856
+ color 150ms ease;
857
+ }
858
+
859
+ .rle-viewtoggle__btn:focus-visible {
860
+ outline: none;
861
+ box-shadow: 0 0 0 2px var(--rle-ring);
862
+ }
863
+
864
+ .rle-viewtoggle__btn--active {
865
+ background: var(--rle-primary);
866
+ color: var(--rle-primary-contrast);
867
+ }
868
+
869
+ /* -----------------------------------------------------------------------
870
+ * Bottom sheet -- dependency-free modal used for the mobile filters panel.
871
+ * --------------------------------------------------------------------- */
872
+
873
+ .rle-sheet-backdrop {
874
+ position: fixed;
875
+ inset: 0;
876
+ z-index: 40;
877
+ background: rgba(0, 0, 0, 0.4);
878
+ opacity: 0;
879
+ transition: opacity 200ms ease;
880
+ }
881
+
882
+ .rle-sheet-backdrop--open {
883
+ opacity: 1;
884
+ }
885
+
886
+ .rle-sheet {
887
+ position: fixed;
888
+ right: 0;
889
+ bottom: 0;
890
+ left: 0;
891
+ z-index: 41;
892
+ display: flex;
893
+ max-height: 85vh;
894
+ flex-direction: column;
895
+ background: var(--rle-surface);
896
+ border-top-left-radius: var(--rle-radius);
897
+ border-top-right-radius: var(--rle-radius);
898
+ box-shadow: var(--rle-shadow);
899
+ transform: translateY(100%);
900
+ transition: transform 200ms ease;
901
+ padding-bottom: env(safe-area-inset-bottom);
902
+ }
903
+
904
+ .rle-sheet--open {
905
+ transform: translateY(0);
906
+ }
907
+
908
+ .rle-sheet__handle {
909
+ flex-shrink: 0;
910
+ width: 36px;
911
+ height: 4px;
912
+ margin: 10px auto 4px;
913
+ background: var(--rle-border);
914
+ border-radius: var(--rle-radius-pill);
915
+ }
916
+
917
+ .rle-sheet__header {
918
+ display: flex;
919
+ flex-shrink: 0;
920
+ align-items: center;
921
+ justify-content: space-between;
922
+ padding: 4px var(--rle-space) var(--rle-space);
923
+ }
924
+
925
+ .rle-sheet__title {
926
+ font-size: var(--rle-fs-lg);
927
+ font-weight: 600;
928
+ color: var(--rle-fg);
929
+ }
930
+
931
+ .rle-sheet__close {
932
+ display: inline-flex;
933
+ align-items: center;
934
+ justify-content: center;
935
+ width: 32px;
936
+ height: 32px;
937
+ padding: 0;
938
+ background: transparent;
939
+ border: none;
940
+ border-radius: var(--rle-radius-sm);
941
+ color: var(--rle-muted);
942
+ cursor: pointer;
943
+ transition:
944
+ background-color 150ms ease,
945
+ color 150ms ease;
946
+ }
947
+
948
+ .rle-sheet__close:hover {
949
+ background: var(--rle-muted-bg);
950
+ color: var(--rle-fg);
951
+ }
952
+
953
+ .rle-sheet__close:focus-visible {
954
+ outline: none;
955
+ box-shadow: 0 0 0 2px var(--rle-ring);
956
+ }
957
+
958
+ .rle-sheet__body {
959
+ flex: 1;
960
+ overflow-y: auto;
961
+ padding: 0 var(--rle-space) var(--rle-space);
962
+ }
963
+
964
+ .rle-sheet__footer {
965
+ display: flex;
966
+ flex-shrink: 0;
967
+ align-items: center;
968
+ justify-content: space-between;
969
+ gap: var(--rle-space);
970
+ padding: var(--rle-space);
971
+ border-top: 1px solid var(--rle-border);
972
+ }