@stapel/search-react 0.13.0 → 0.14.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.
- package/CHANGELOG.md +51 -0
- package/dist/default/LocationSummaryLine.d.ts +19 -0
- package/dist/default/LocationSummaryLine.d.ts.map +1 -1
- package/dist/default/LocationSummaryLine.js +111 -11
- package/dist/default/LocationSummaryLine.js.map +1 -1
- package/llms.txt +2 -2
- package/manifest.json +3 -1
- package/nav-manifest.json +1 -1
- package/package.json +3 -3
- package/src/analytics/generated/events.json +1 -1
- package/src/default/LocationSummaryLine.tsx +150 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @stapel/search-react
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 396e32e: The phone SERP's location row is one compact line again, and stops overlapping itself.
|
|
8
|
+
|
|
9
|
+
Measured on a live 390×844 SERP, in both themes, on every result page — the
|
|
10
|
+
category listing, the text search and every filter slice:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
{"kind":"clipped-left","t":"A chosen place on the map","x":-4}
|
|
14
|
+
{"kind":"overlap","a":"· Within 25 km","b":"Filters","px":43}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
On screen that read as "…hosen place on the map · Within 25 kmlters", with the
|
|
18
|
+
red active-filter plaque floating in the top-right corner of the page attached
|
|
19
|
+
to nothing. At 1440 it was clean, which is why nothing before this caught it.
|
|
20
|
+
|
|
21
|
+
**Three causes, none of them the flex row itself.**
|
|
22
|
+
|
|
23
|
+
1. An antd `<Button>` CENTRES its content. `minWidth: 0` let the left half
|
|
24
|
+
shrink and nothing clipped what was inside it, so the label overflowed its
|
|
25
|
+
box symmetrically — off the left edge of the screen at `x = -4` and 43px
|
|
26
|
+
across the word "Filters" at the other end. A shrunk box with no `overflow`
|
|
27
|
+
is not a truncation, it is an overlap.
|
|
28
|
+
2. Nothing declared which end may shrink. Both were `1 1 auto`, so a long place
|
|
29
|
+
name took width from a word that must never lose any.
|
|
30
|
+
3. The count was an antd `<Badge count>` — an absolutely positioned `sup` hung
|
|
31
|
+
off the top-right CORNER of what it wraps. At the trailing edge of a
|
|
32
|
+
full-width row that puts it outside the row entirely.
|
|
33
|
+
|
|
34
|
+
**Now:** `place · radius` left, `Filters` right, on one line. The left half is
|
|
35
|
+
the only one that grows or shrinks and it truncates with an ellipsis (`display:
|
|
36
|
+
block` on the label is load-bearing — `text-overflow` does nothing on a flex
|
|
37
|
+
box, which is how the first attempt cut the name mid-glyph); the right half is
|
|
38
|
+
`flex: 0 0 auto`; the count is a pill IN the flow beside the word it counts
|
|
39
|
+
for. The place and the radius now share ONE truncating label, so the radius can
|
|
40
|
+
no longer travel past the label's end on its own.
|
|
41
|
+
|
|
42
|
+
The rules that had to reach inside the button ship as a hoisted stylesheet
|
|
43
|
+
(`locationLineCss`, the pattern `<ListingCard>` and `<SkinCarousel>` use),
|
|
44
|
+
since a descendant rule is not expressible as an inline style.
|
|
45
|
+
|
|
46
|
+
`search-location-filters-badge` is now the trailing group rather than an antd
|
|
47
|
+
`<Badge>`; the count carries `data-testid="search-location-filters-count"`, and
|
|
48
|
+
the truncating label `data-testid="search-location-label"`.
|
|
49
|
+
|
|
50
|
+
A new skin demo variant, `long-place`, photographs the measured case: a place
|
|
51
|
+
name a geocoder really returns, at 390px, with a radius and a count on the same
|
|
52
|
+
line.
|
|
53
|
+
|
|
3
54
|
## 0.13.0
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from "react";
|
|
2
2
|
import type { GeoFilterSlotProps } from "./FacetPanelPane.js";
|
|
3
|
+
/** The class the row carries, for {@link locationLineCss}. */
|
|
4
|
+
export declare const LOCATION_LINE_CLASS = "stapel-search-location-line";
|
|
5
|
+
/** The class the shrinking half carries. */
|
|
6
|
+
export declare const LOCATION_LINE_WHERE_CLASS = "stapel-search-location-line-where";
|
|
7
|
+
/** The class the truncating label carries. */
|
|
8
|
+
export declare const LOCATION_LINE_LABEL_CLASS = "stapel-search-location-line-label";
|
|
9
|
+
/** The class the fixed half carries. */
|
|
10
|
+
export declare const LOCATION_LINE_END_CLASS = "stapel-search-location-line-end";
|
|
11
|
+
/** The `href` the hoisted stylesheet is deduplicated by. */
|
|
12
|
+
export declare const LOCATION_LINE_STYLE_HREF = "stapel-search-location-line";
|
|
13
|
+
/**
|
|
14
|
+
* The rules that keep the line ONE line — and that an inline style cannot
|
|
15
|
+
* reach, because they belong on the `<span>` antd wraps a button's children
|
|
16
|
+
* in. See the file header (defect C12) for what each of them stops.
|
|
17
|
+
*
|
|
18
|
+
* Static: nothing here varies per instance, so one hoisted copy serves a page
|
|
19
|
+
* in either theme.
|
|
20
|
+
*/
|
|
21
|
+
export declare function locationLineCss(): string;
|
|
3
22
|
export interface LocationSummaryLineProps {
|
|
4
23
|
/**
|
|
5
24
|
* The location control the sheet opens onto — the same slot the panel and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocationSummaryLine.d.ts","sourceRoot":"","sources":["../../src/default/LocationSummaryLine.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LocationSummaryLine.d.ts","sourceRoot":"","sources":["../../src/default/LocationSummaryLine.tsx"],"names":[],"mappings":"AAkEA,OAAO,KAAK,EAAiB,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAOpE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAG9D,8DAA8D;AAC9D,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AACjE,4CAA4C;AAC5C,eAAO,MAAM,yBAAyB,sCAAsC,CAAC;AAC7E,8CAA8C;AAC9C,eAAO,MAAM,yBAAyB,sCAAsC,CAAC;AAC7E,wCAAwC;AACxC,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE,4DAA4D;AAC5D,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AAEtE;;;;;;;GAOG;AACH,wBAAgB,eAAe,IAAI,MAAM,CA4BxC;AAsCD,MAAM,WAAW,wBAAwB;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,SAAS,CAAC;IACnE;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC;CAChC;AAyBD,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,wBAAwB,GAC9B,YAAY,CA+Gd"}
|
|
@@ -28,29 +28,129 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
28
28
|
*
|
|
29
29
|
* `<FilterChips>`'s leading chip is a 32px circle: a number inside it is a
|
|
30
30
|
* number nobody reads, so it shows a dot. This is a full-width row with a word
|
|
31
|
-
* on it, so
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* on it, so it has room to say HOW MANY constraints are applied — which is the
|
|
32
|
+
* difference between "something is filtered" and "four things are, and that is
|
|
33
|
+
* why there are three results". Both read the same `activeFilters` off the URL
|
|
34
|
+
* state; neither invents a second counter.
|
|
35
|
+
*
|
|
36
|
+
* ## The row overlapped itself on a phone, and why (defect C12)
|
|
37
|
+
*
|
|
38
|
+
* Measured on a live 390px SERP, in both themes, on every result page:
|
|
39
|
+
*
|
|
40
|
+
* ```
|
|
41
|
+
* {"kind":"clipped-left","t":"A chosen place on the map","x":-4}
|
|
42
|
+
* {"kind":"overlap","a":"· Within 25 km","b":"Filters","px":43}
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* Three separate causes, and none of them was the flex row itself:
|
|
46
|
+
*
|
|
47
|
+
* 1. **An antd `<Button>` CENTRES its content.** `minWidth: 0` let the left
|
|
48
|
+
* item shrink, and nothing clipped what was inside it, so the label
|
|
49
|
+
* overflowed its box symmetrically — off the left edge of the screen at
|
|
50
|
+
* `x = -4` and 43px across the word "Filters" at the other end. A shrunk
|
|
51
|
+
* box with no `overflow` is not a truncation, it is an overlap.
|
|
52
|
+
* 2. **Nothing declared which end may shrink.** Both ends were `1 1 auto`, so
|
|
53
|
+
* a long place name took width from a word that must never lose any.
|
|
54
|
+
* 3. **The count was an antd `<Badge count>`,** which is an absolutely
|
|
55
|
+
* positioned `sup` hung off the top-right CORNER of what it wraps. At the
|
|
56
|
+
* trailing edge of a full-width row that lands it outside the row entirely
|
|
57
|
+
* — a red plaque floating in the corner of the page, attached to nothing
|
|
58
|
+
* (measured at `y = 72` for a row whose own line is at `y = 93`).
|
|
59
|
+
*
|
|
60
|
+
* So: one compact line, `place · radius` on the left and `Filters` right; the
|
|
61
|
+
* left is the only half that shrinks and it truncates with an ellipsis; the
|
|
62
|
+
* count rides IN the flow beside the word it counts for. The first two rules
|
|
63
|
+
* are what an inline style cannot reach (they belong on the wrapper antd puts
|
|
64
|
+
* around a button's children), which is why this file hoists a stylesheet the
|
|
65
|
+
* way `<ListingCard>` and `<SkinCarousel>` do.
|
|
35
66
|
*/
|
|
36
67
|
import { useState } from "react";
|
|
37
|
-
import {
|
|
68
|
+
import { Button, Flex } from "antd";
|
|
38
69
|
import { useT } from "@stapel/core";
|
|
39
|
-
import { spacing } from "@stapel/tokens";
|
|
70
|
+
import { cssVar, fontSize, radii, spacing } from "@stapel/tokens";
|
|
40
71
|
import { useSearchState } from "../headless/SearchStateProvider.js";
|
|
41
72
|
import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
42
73
|
import { geoSummaryFallback } from "./FacetPanelPane.js";
|
|
43
74
|
import { GeoSheet, SUMMARY_GEO_TEST_IDS } from "./geoSheet.js";
|
|
75
|
+
/** The class the row carries, for {@link locationLineCss}. */
|
|
76
|
+
export const LOCATION_LINE_CLASS = "stapel-search-location-line";
|
|
77
|
+
/** The class the shrinking half carries. */
|
|
78
|
+
export const LOCATION_LINE_WHERE_CLASS = "stapel-search-location-line-where";
|
|
79
|
+
/** The class the truncating label carries. */
|
|
80
|
+
export const LOCATION_LINE_LABEL_CLASS = "stapel-search-location-line-label";
|
|
81
|
+
/** The class the fixed half carries. */
|
|
82
|
+
export const LOCATION_LINE_END_CLASS = "stapel-search-location-line-end";
|
|
83
|
+
/** The `href` the hoisted stylesheet is deduplicated by. */
|
|
84
|
+
export const LOCATION_LINE_STYLE_HREF = "stapel-search-location-line";
|
|
85
|
+
/**
|
|
86
|
+
* The rules that keep the line ONE line — and that an inline style cannot
|
|
87
|
+
* reach, because they belong on the `<span>` antd wraps a button's children
|
|
88
|
+
* in. See the file header (defect C12) for what each of them stops.
|
|
89
|
+
*
|
|
90
|
+
* Static: nothing here varies per instance, so one hoisted copy serves a page
|
|
91
|
+
* in either theme.
|
|
92
|
+
*/
|
|
93
|
+
export function locationLineCss() {
|
|
94
|
+
const row = `.${LOCATION_LINE_CLASS}`;
|
|
95
|
+
const where = `.${LOCATION_LINE_WHERE_CLASS}`;
|
|
96
|
+
const label = `.${LOCATION_LINE_LABEL_CLASS}`;
|
|
97
|
+
const end = `.${LOCATION_LINE_END_CLASS}`;
|
|
98
|
+
return [
|
|
99
|
+
// `min-width:0` on the row too: a flex item's default `min-width:auto`
|
|
100
|
+
// is what makes a nested flex row refuse to shrink at all.
|
|
101
|
+
`${row}{min-inline-size:0}`,
|
|
102
|
+
// The ONLY half that shrinks, and it CLIPS what it cannot show. An antd
|
|
103
|
+
// Button is already a flex row and it CENTRES its children, so without
|
|
104
|
+
// these two the box shrank and its content overflowed both edges at once
|
|
105
|
+
// — the whole defect: `x:-4` on the left, 43px over "Filters" on the
|
|
106
|
+
// right.
|
|
107
|
+
`${where}{flex:1 1 auto;min-inline-size:0;overflow:hidden;` +
|
|
108
|
+
`justify-content:flex-start}`,
|
|
109
|
+
// Whatever the button puts its children in has to be allowed to shrink;
|
|
110
|
+
// a flex item's `min-width:auto` refuses to go below its content.
|
|
111
|
+
`${where}>span{min-inline-size:0}`,
|
|
112
|
+
// `display:block` is load-bearing: `text-overflow` applies to a BLOCK
|
|
113
|
+
// container and does nothing on a flex one, so a label that had been
|
|
114
|
+
// turned into a flex box truncated with a hard cut mid-glyph instead of
|
|
115
|
+
// an ellipsis.
|
|
116
|
+
`${label}{display:block;min-inline-size:0;overflow:hidden;` +
|
|
117
|
+
`text-overflow:ellipsis;white-space:nowrap}`,
|
|
118
|
+
// The word a person is looking for never loses a pixel to a place name.
|
|
119
|
+
`${end}{flex:0 0 auto}`,
|
|
120
|
+
].join("");
|
|
121
|
+
}
|
|
44
122
|
/** The row. Both ends are text buttons, so the row reads as a line of type
|
|
45
123
|
* rather than as two controls bolted onto a results page. */
|
|
46
124
|
const ROW = { width: "100%" };
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
125
|
+
/** The shrinking half. The flex rules it needs live in the hoisted sheet
|
|
126
|
+
* (they have to reach antd's own wrapper span); this is the chrome. */
|
|
49
127
|
const LOCATION = {
|
|
50
|
-
minWidth: 0,
|
|
51
128
|
paddingInline: 0,
|
|
52
129
|
textAlign: "start",
|
|
53
130
|
};
|
|
131
|
+
/** The pin, which is not allowed to shrink either — a squashed glyph is
|
|
132
|
+
* noise, and it costs 16px. */
|
|
133
|
+
const PIN = { flex: "0 0 auto", display: "inline-flex" };
|
|
134
|
+
/**
|
|
135
|
+
* The count, IN the flow.
|
|
136
|
+
*
|
|
137
|
+
* An antd `<Badge count>` is an absolutely positioned `sup` on the corner of
|
|
138
|
+
* whatever it wraps, and at the trailing edge of a full-width row that puts it
|
|
139
|
+
* outside the row: a red plaque in the corner of the page attached to nothing.
|
|
140
|
+
* A pill beside the word is the same fact, in the line it belongs to, and it
|
|
141
|
+
* takes part in the layout instead of floating over it.
|
|
142
|
+
*/
|
|
143
|
+
const COUNT = {
|
|
144
|
+
display: "inline-block",
|
|
145
|
+
minInlineSize: `${String(spacing[4])}px`,
|
|
146
|
+
paddingInline: spacing[1],
|
|
147
|
+
borderRadius: radii.full,
|
|
148
|
+
background: cssVar("brand"),
|
|
149
|
+
color: cssVar("text-on-accent"),
|
|
150
|
+
fontSize: fontSize.xs.fontSize,
|
|
151
|
+
lineHeight: `${String(spacing[4])}px`,
|
|
152
|
+
textAlign: "center",
|
|
153
|
+
};
|
|
54
154
|
/** A map pin in `currentColor` — the house convention: an inline monochrome
|
|
55
155
|
* SVG, no icon-font dependency, and it inherits the theme rather than carrying
|
|
56
156
|
* a colour. `aria-hidden` because the text beside it is the name. */
|
|
@@ -72,9 +172,9 @@ export function LocationSummaryLine(props) {
|
|
|
72
172
|
const where = geo === undefined
|
|
73
173
|
? t(SEARCH_I18N_KEYS.geoEverywhere)
|
|
74
174
|
: (props.geoLabel ?? geoSummaryFallback(geo, t));
|
|
75
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Flex, { align: "center", justify: "space-between", gap: spacing[2], style: ROW, "data-testid": "search-location-summary", "data-geo": geo === undefined ? "off" : "on", children: [_jsxs(Button, { type: "link", style: LOCATION,
|
|
175
|
+
return (_jsxs(_Fragment, { children: [_jsx("style", { href: LOCATION_LINE_STYLE_HREF, precedence: "default", children: locationLineCss() }), _jsxs(Flex, { align: "center", justify: "space-between", gap: spacing[2], style: ROW, className: LOCATION_LINE_CLASS, "data-testid": "search-location-summary", "data-geo": geo === undefined ? "off" : "on", children: [_jsxs(Button, { type: "link", style: LOCATION, className: LOCATION_LINE_WHERE_CLASS, "data-testid": "search-location-open", "data-analytics": "none", "data-analytics-reason": "opening the location sheet is a read, not a flow step", onClick: () => {
|
|
76
176
|
setOpen(true);
|
|
77
|
-
}, children: [where, radius !== undefined && (_jsxs("span", { "data-testid": "search-location-radius", children: [" · ", radius] }))] }),
|
|
177
|
+
}, children: [_jsx("span", { style: PIN, children: _jsx(PinGlyph, {}) }), _jsxs("span", { className: LOCATION_LINE_LABEL_CLASS, "data-testid": "search-location-label", children: [where, radius !== undefined && (_jsxs("span", { "data-testid": "search-location-radius", children: [" · ", radius] }))] })] }), _jsxs(Flex, { align: "center", gap: spacing[1], className: LOCATION_LINE_END_CLASS, "data-testid": "search-location-filters-badge", children: [_jsx(Button, { type: "link", style: { paddingInline: 0 }, "data-testid": "search-location-filters", "data-active": activeFilters > 0 ? "true" : "false", "data-analytics": "none", "data-analytics-reason": "opening the filter sheet is a read, not a flow step", onClick: props.onOpenAll, children: t(SEARCH_I18N_KEYS.filtersShort) }), activeFilters > 0 && (_jsx("span", { style: COUNT, "data-testid": "search-location-filters-count", children: activeFilters }))] })] }), _jsx(GeoSheet, { open: open, onClose: () => {
|
|
78
178
|
setOpen(false);
|
|
79
179
|
}, testIds: SUMMARY_GEO_TEST_IDS, ...(props.renderGeoFilter !== undefined
|
|
80
180
|
? { renderGeoFilter: props.renderGeoFilter }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocationSummaryLine.js","sourceRoot":"","sources":["../../src/default/LocationSummaryLine.tsx"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"LocationSummaryLine.js","sourceRoot":"","sources":["../../src/default/LocationSummaryLine.tsx"],"names":[],"mappings":"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"}
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @stapel/search-react 0.
|
|
1
|
+
# @stapel/search-react 0.14.0
|
|
2
2
|
|
|
3
3
|
Headless React flow pair for stapel-search (contract >=0.7 <0.8) — business + state, zero visual opinion.
|
|
4
4
|
Built on @stapel/core: typed client + StapelApiError envelope, auth token refresh,
|
|
@@ -81,7 +81,7 @@ const { tracked } = useTracked();
|
|
|
81
81
|
- search.facet-panel-pane → <FacetPanelPane> [open-search|narrowed] demo/FacetPanelPane.demo.tsx
|
|
82
82
|
- search.filter-sheet → <SearchPage> [open] demo/FilterSheet.demo.tsx
|
|
83
83
|
- search.language-select → <LanguageSelect> [offered|from-a-link] demo/LanguageSelect.demo.tsx
|
|
84
|
-
- search.location-summary → <LocationSummaryLine> [everywhere|placed|narrowed|wide] demo/LocationSummaryLine.demo.tsx
|
|
84
|
+
- search.location-summary → <LocationSummaryLine> [everywhere|placed|narrowed|long-place|wide] demo/LocationSummaryLine.demo.tsx
|
|
85
85
|
- search.page → <SearchPage> [desktop|phone|unreadable-link] demo/SearchPage.demo.tsx
|
|
86
86
|
- search.page-size-select → <PageSizeSelect> [ladder|off-ladder] demo/PageSizeSelect.demo.tsx
|
|
87
87
|
- search.range-filter-row → <RangeFilterRow> [untouched|applied] demo/RangeFilterRow.demo.tsx
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$generated": "by scripts/gen-manifest.mjs — do not edit; drift-gated (pnpm gen:manifest:check)",
|
|
3
3
|
"package": "@stapel/search-react",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"backend": {
|
|
6
6
|
"module": "stapel-search",
|
|
7
7
|
"contract": ">=0.7 <0.8"
|
|
@@ -506,12 +506,14 @@
|
|
|
506
506
|
],
|
|
507
507
|
"tokens": [
|
|
508
508
|
"brand",
|
|
509
|
+
"text-on-accent",
|
|
509
510
|
"text-muted"
|
|
510
511
|
],
|
|
511
512
|
"variants": [
|
|
512
513
|
"everywhere",
|
|
513
514
|
"placed",
|
|
514
515
|
"narrowed",
|
|
516
|
+
"long-place",
|
|
515
517
|
"wide"
|
|
516
518
|
],
|
|
517
519
|
"source": "demo/LocationSummaryLine.demo.tsx"
|
package/nav-manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stapel/search-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Headless React pair for stapel-search: a typed query client, TanStack Query hooks, and a URL-first state codec that makes a search shareable by construction (filters, ranges, geo, sort and the keyset cursor all live in the query string). Drill-down facets rendered with their remaining counts and with the server's own honesty flags — approximate, skipped, degraded — never swallowed; keyset pagination with the window refusal named; DSA Art. 26 `promoted` marking carried into every card slot and the P2B Art. 5 ranking disclosure exposed as data. Zero visual opinion in the main entry; an opt-in /default subpath ships the antd skin, and /router binds the codec to react-router's useSearchParams.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -115,10 +115,10 @@
|
|
|
115
115
|
"size-limit": "^11.2.0",
|
|
116
116
|
"typescript": "^5.8.3",
|
|
117
117
|
"vitest": "^3.2.4",
|
|
118
|
-
"@stapel/attributes-react": "^0.6.0",
|
|
119
|
-
"@stapel/core": "^0.22.0",
|
|
120
118
|
"@stapel/image": "^0.4.2",
|
|
119
|
+
"@stapel/core": "^0.22.0",
|
|
121
120
|
"@stapel/showcase": "^0.3.0",
|
|
121
|
+
"@stapel/attributes-react": "^0.6.0",
|
|
122
122
|
"@stapel/tokens": "^0.6.0",
|
|
123
123
|
"@stapel/tokens-antd": "^0.11.0"
|
|
124
124
|
},
|
|
@@ -27,34 +27,139 @@
|
|
|
27
27
|
*
|
|
28
28
|
* `<FilterChips>`'s leading chip is a 32px circle: a number inside it is a
|
|
29
29
|
* number nobody reads, so it shows a dot. This is a full-width row with a word
|
|
30
|
-
* on it, so
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* on it, so it has room to say HOW MANY constraints are applied — which is the
|
|
31
|
+
* difference between "something is filtered" and "four things are, and that is
|
|
32
|
+
* why there are three results". Both read the same `activeFilters` off the URL
|
|
33
|
+
* state; neither invents a second counter.
|
|
34
|
+
*
|
|
35
|
+
* ## The row overlapped itself on a phone, and why (defect C12)
|
|
36
|
+
*
|
|
37
|
+
* Measured on a live 390px SERP, in both themes, on every result page:
|
|
38
|
+
*
|
|
39
|
+
* ```
|
|
40
|
+
* {"kind":"clipped-left","t":"A chosen place on the map","x":-4}
|
|
41
|
+
* {"kind":"overlap","a":"· Within 25 km","b":"Filters","px":43}
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Three separate causes, and none of them was the flex row itself:
|
|
45
|
+
*
|
|
46
|
+
* 1. **An antd `<Button>` CENTRES its content.** `minWidth: 0` let the left
|
|
47
|
+
* item shrink, and nothing clipped what was inside it, so the label
|
|
48
|
+
* overflowed its box symmetrically — off the left edge of the screen at
|
|
49
|
+
* `x = -4` and 43px across the word "Filters" at the other end. A shrunk
|
|
50
|
+
* box with no `overflow` is not a truncation, it is an overlap.
|
|
51
|
+
* 2. **Nothing declared which end may shrink.** Both ends were `1 1 auto`, so
|
|
52
|
+
* a long place name took width from a word that must never lose any.
|
|
53
|
+
* 3. **The count was an antd `<Badge count>`,** which is an absolutely
|
|
54
|
+
* positioned `sup` hung off the top-right CORNER of what it wraps. At the
|
|
55
|
+
* trailing edge of a full-width row that lands it outside the row entirely
|
|
56
|
+
* — a red plaque floating in the corner of the page, attached to nothing
|
|
57
|
+
* (measured at `y = 72` for a row whose own line is at `y = 93`).
|
|
58
|
+
*
|
|
59
|
+
* So: one compact line, `place · radius` on the left and `Filters` right; the
|
|
60
|
+
* left is the only half that shrinks and it truncates with an ellipsis; the
|
|
61
|
+
* count rides IN the flow beside the word it counts for. The first two rules
|
|
62
|
+
* are what an inline style cannot reach (they belong on the wrapper antd puts
|
|
63
|
+
* around a button's children), which is why this file hoists a stylesheet the
|
|
64
|
+
* way `<ListingCard>` and `<SkinCarousel>` do.
|
|
34
65
|
*/
|
|
35
66
|
import { useState } from "react";
|
|
36
67
|
import type { CSSProperties, ReactElement, ReactNode } from "react";
|
|
37
|
-
import {
|
|
68
|
+
import { Button, Flex } from "antd";
|
|
38
69
|
import { useT } from "@stapel/core";
|
|
39
|
-
import { spacing } from "@stapel/tokens";
|
|
70
|
+
import { cssVar, fontSize, radii, spacing } from "@stapel/tokens";
|
|
40
71
|
import { useSearchState } from "../headless/SearchStateProvider.js";
|
|
41
72
|
import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
42
73
|
import { geoSummaryFallback } from "./FacetPanelPane.js";
|
|
43
74
|
import type { GeoFilterSlotProps } from "./FacetPanelPane.js";
|
|
44
75
|
import { GeoSheet, SUMMARY_GEO_TEST_IDS } from "./geoSheet.js";
|
|
45
76
|
|
|
77
|
+
/** The class the row carries, for {@link locationLineCss}. */
|
|
78
|
+
export const LOCATION_LINE_CLASS = "stapel-search-location-line";
|
|
79
|
+
/** The class the shrinking half carries. */
|
|
80
|
+
export const LOCATION_LINE_WHERE_CLASS = "stapel-search-location-line-where";
|
|
81
|
+
/** The class the truncating label carries. */
|
|
82
|
+
export const LOCATION_LINE_LABEL_CLASS = "stapel-search-location-line-label";
|
|
83
|
+
/** The class the fixed half carries. */
|
|
84
|
+
export const LOCATION_LINE_END_CLASS = "stapel-search-location-line-end";
|
|
85
|
+
|
|
86
|
+
/** The `href` the hoisted stylesheet is deduplicated by. */
|
|
87
|
+
export const LOCATION_LINE_STYLE_HREF = "stapel-search-location-line";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The rules that keep the line ONE line — and that an inline style cannot
|
|
91
|
+
* reach, because they belong on the `<span>` antd wraps a button's children
|
|
92
|
+
* in. See the file header (defect C12) for what each of them stops.
|
|
93
|
+
*
|
|
94
|
+
* Static: nothing here varies per instance, so one hoisted copy serves a page
|
|
95
|
+
* in either theme.
|
|
96
|
+
*/
|
|
97
|
+
export function locationLineCss(): string {
|
|
98
|
+
const row = `.${LOCATION_LINE_CLASS}`;
|
|
99
|
+
const where = `.${LOCATION_LINE_WHERE_CLASS}`;
|
|
100
|
+
const label = `.${LOCATION_LINE_LABEL_CLASS}`;
|
|
101
|
+
const end = `.${LOCATION_LINE_END_CLASS}`;
|
|
102
|
+
return [
|
|
103
|
+
// `min-width:0` on the row too: a flex item's default `min-width:auto`
|
|
104
|
+
// is what makes a nested flex row refuse to shrink at all.
|
|
105
|
+
`${row}{min-inline-size:0}`,
|
|
106
|
+
// The ONLY half that shrinks, and it CLIPS what it cannot show. An antd
|
|
107
|
+
// Button is already a flex row and it CENTRES its children, so without
|
|
108
|
+
// these two the box shrank and its content overflowed both edges at once
|
|
109
|
+
// — the whole defect: `x:-4` on the left, 43px over "Filters" on the
|
|
110
|
+
// right.
|
|
111
|
+
`${where}{flex:1 1 auto;min-inline-size:0;overflow:hidden;` +
|
|
112
|
+
`justify-content:flex-start}`,
|
|
113
|
+
// Whatever the button puts its children in has to be allowed to shrink;
|
|
114
|
+
// a flex item's `min-width:auto` refuses to go below its content.
|
|
115
|
+
`${where}>span{min-inline-size:0}`,
|
|
116
|
+
// `display:block` is load-bearing: `text-overflow` applies to a BLOCK
|
|
117
|
+
// container and does nothing on a flex one, so a label that had been
|
|
118
|
+
// turned into a flex box truncated with a hard cut mid-glyph instead of
|
|
119
|
+
// an ellipsis.
|
|
120
|
+
`${label}{display:block;min-inline-size:0;overflow:hidden;` +
|
|
121
|
+
`text-overflow:ellipsis;white-space:nowrap}`,
|
|
122
|
+
// The word a person is looking for never loses a pixel to a place name.
|
|
123
|
+
`${end}{flex:0 0 auto}`,
|
|
124
|
+
].join("");
|
|
125
|
+
}
|
|
126
|
+
|
|
46
127
|
/** The row. Both ends are text buttons, so the row reads as a line of type
|
|
47
128
|
* rather than as two controls bolted onto a results page. */
|
|
48
129
|
const ROW: CSSProperties = { width: "100%" };
|
|
49
130
|
|
|
50
|
-
/**
|
|
51
|
-
*
|
|
131
|
+
/** The shrinking half. The flex rules it needs live in the hoisted sheet
|
|
132
|
+
* (they have to reach antd's own wrapper span); this is the chrome. */
|
|
52
133
|
const LOCATION: CSSProperties = {
|
|
53
|
-
minWidth: 0,
|
|
54
134
|
paddingInline: 0,
|
|
55
135
|
textAlign: "start",
|
|
56
136
|
};
|
|
57
137
|
|
|
138
|
+
/** The pin, which is not allowed to shrink either — a squashed glyph is
|
|
139
|
+
* noise, and it costs 16px. */
|
|
140
|
+
const PIN: CSSProperties = { flex: "0 0 auto", display: "inline-flex" };
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The count, IN the flow.
|
|
144
|
+
*
|
|
145
|
+
* An antd `<Badge count>` is an absolutely positioned `sup` on the corner of
|
|
146
|
+
* whatever it wraps, and at the trailing edge of a full-width row that puts it
|
|
147
|
+
* outside the row: a red plaque in the corner of the page attached to nothing.
|
|
148
|
+
* A pill beside the word is the same fact, in the line it belongs to, and it
|
|
149
|
+
* takes part in the layout instead of floating over it.
|
|
150
|
+
*/
|
|
151
|
+
const COUNT: CSSProperties = {
|
|
152
|
+
display: "inline-block",
|
|
153
|
+
minInlineSize: `${String(spacing[4])}px`,
|
|
154
|
+
paddingInline: spacing[1],
|
|
155
|
+
borderRadius: radii.full,
|
|
156
|
+
background: cssVar("brand"),
|
|
157
|
+
color: cssVar("text-on-accent"),
|
|
158
|
+
fontSize: fontSize.xs.fontSize,
|
|
159
|
+
lineHeight: `${String(spacing[4])}px`,
|
|
160
|
+
textAlign: "center",
|
|
161
|
+
};
|
|
162
|
+
|
|
58
163
|
export interface LocationSummaryLineProps {
|
|
59
164
|
/**
|
|
60
165
|
* The location control the sheet opens onto — the same slot the panel and
|
|
@@ -123,18 +228,26 @@ export function LocationSummaryLine(
|
|
|
123
228
|
|
|
124
229
|
return (
|
|
125
230
|
<>
|
|
231
|
+
<style href={LOCATION_LINE_STYLE_HREF} precedence="default">
|
|
232
|
+
{locationLineCss()}
|
|
233
|
+
</style>
|
|
126
234
|
<Flex
|
|
127
235
|
align="center"
|
|
128
236
|
justify="space-between"
|
|
129
237
|
gap={spacing[2]}
|
|
130
238
|
style={ROW}
|
|
239
|
+
className={LOCATION_LINE_CLASS}
|
|
131
240
|
data-testid="search-location-summary"
|
|
132
241
|
data-geo={geo === undefined ? "off" : "on"}
|
|
133
242
|
>
|
|
243
|
+
{/* The glyph is rendered as a CHILD rather than through antd's `icon`
|
|
244
|
+
prop: the icon slot sits outside the wrapper span, so the label
|
|
245
|
+
beside it could not be given a min-width of its own — and a label
|
|
246
|
+
that cannot shrink is a label that overflows. */}
|
|
134
247
|
<Button
|
|
135
248
|
type="link"
|
|
136
249
|
style={LOCATION}
|
|
137
|
-
|
|
250
|
+
className={LOCATION_LINE_WHERE_CLASS}
|
|
138
251
|
data-testid="search-location-open"
|
|
139
252
|
data-analytics="none"
|
|
140
253
|
data-analytics-reason="opening the location sheet is a read, not a flow step"
|
|
@@ -142,13 +255,21 @@ export function LocationSummaryLine(
|
|
|
142
255
|
setOpen(true);
|
|
143
256
|
}}
|
|
144
257
|
>
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
258
|
+
<span style={PIN}>
|
|
259
|
+
<PinGlyph />
|
|
260
|
+
</span>
|
|
261
|
+
<span
|
|
262
|
+
className={LOCATION_LINE_LABEL_CLASS}
|
|
263
|
+
data-testid="search-location-label"
|
|
264
|
+
>
|
|
265
|
+
{where}
|
|
266
|
+
{radius !== undefined && (
|
|
267
|
+
<span data-testid="search-location-radius">
|
|
268
|
+
{" · "}
|
|
269
|
+
{radius}
|
|
270
|
+
</span>
|
|
271
|
+
)}
|
|
272
|
+
</span>
|
|
152
273
|
</Button>
|
|
153
274
|
|
|
154
275
|
{/* "Filters", not "All filters": this end of the row shares 390px
|
|
@@ -156,10 +277,12 @@ export function LocationSummaryLine(
|
|
|
156
277
|
the person is looking for is the noun. The panel's own heading
|
|
157
278
|
still says "All filters" — there it is naming a sheet, not a
|
|
158
279
|
door. */}
|
|
159
|
-
{/* The count, not a dot: this row has the width to say how many
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
280
|
+
{/* The count, not a dot: this row has the width to say how many — and
|
|
281
|
+
it rides IN the line rather than floating off its corner. */}
|
|
282
|
+
<Flex
|
|
283
|
+
align="center"
|
|
284
|
+
gap={spacing[1]}
|
|
285
|
+
className={LOCATION_LINE_END_CLASS}
|
|
163
286
|
data-testid="search-location-filters-badge"
|
|
164
287
|
>
|
|
165
288
|
<Button
|
|
@@ -173,7 +296,12 @@ export function LocationSummaryLine(
|
|
|
173
296
|
>
|
|
174
297
|
{t(SEARCH_I18N_KEYS.filtersShort)}
|
|
175
298
|
</Button>
|
|
176
|
-
|
|
299
|
+
{activeFilters > 0 && (
|
|
300
|
+
<span style={COUNT} data-testid="search-location-filters-count">
|
|
301
|
+
{activeFilters}
|
|
302
|
+
</span>
|
|
303
|
+
)}
|
|
304
|
+
</Flex>
|
|
177
305
|
</Flex>
|
|
178
306
|
|
|
179
307
|
<GeoSheet
|