@stapel/search-react 0.39.0 → 0.40.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 (36) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +85 -6
  3. package/dist/default/FacetGroupControl.d.ts.map +1 -1
  4. package/dist/default/FacetGroupControl.js +31 -4
  5. package/dist/default/FacetGroupControl.js.map +1 -1
  6. package/dist/default/FacetPanelPane.d.ts +24 -0
  7. package/dist/default/FacetPanelPane.d.ts.map +1 -1
  8. package/dist/default/FacetPanelPane.js +27 -6
  9. package/dist/default/FacetPanelPane.js.map +1 -1
  10. package/dist/default/SearchPage.d.ts +33 -1
  11. package/dist/default/SearchPage.d.ts.map +1 -1
  12. package/dist/default/SearchPage.js +32 -5
  13. package/dist/default/SearchPage.js.map +1 -1
  14. package/dist/default/SearchResultsPane.d.ts +76 -0
  15. package/dist/default/SearchResultsPane.d.ts.map +1 -1
  16. package/dist/default/SearchResultsPane.js +132 -45
  17. package/dist/default/SearchResultsPane.js.map +1 -1
  18. package/dist/default/index.d.ts +4 -2
  19. package/dist/default/index.d.ts.map +1 -1
  20. package/dist/default/index.js +2 -1
  21. package/dist/default/index.js.map +1 -1
  22. package/dist/default/swatches.d.ts +44 -0
  23. package/dist/default/swatches.d.ts.map +1 -0
  24. package/dist/default/swatches.js +200 -0
  25. package/dist/default/swatches.js.map +1 -0
  26. package/llms.txt +2 -2
  27. package/manifest.json +3 -1
  28. package/nav-manifest.json +1 -1
  29. package/package.json +5 -5
  30. package/src/analytics/generated/events.json +1 -1
  31. package/src/default/FacetGroupControl.tsx +39 -0
  32. package/src/default/FacetPanelPane.tsx +61 -5
  33. package/src/default/SearchPage.tsx +72 -1
  34. package/src/default/SearchResultsPane.tsx +134 -5
  35. package/src/default/index.ts +9 -0
  36. package/src/default/swatches.ts +214 -0
@@ -86,7 +86,12 @@ export {
86
86
  RESULTS_COLUMNS_CLASS,
87
87
  RESULTS_COLUMNS_STYLE_HREF,
88
88
  RESULTS_TOOLBAR_CLASS,
89
+ RESULTS_TOOLBAR_STICKY_CLASS,
90
+ RESULTS_TOOLBAR_STYLE_HREF,
91
+ RESULTS_TOOLBAR_TOP_VAR,
89
92
  resultsColumnsCss,
93
+ toolbarRowMinHeight,
94
+ toolbarStickyCss,
90
95
  } from "./SearchResultsPane.js";
91
96
  export type {
92
97
  ResultsColumns,
@@ -151,6 +156,9 @@ export type {
151
156
  FacetOptionNode,
152
157
  } from "./FacetGroupControl.js";
153
158
 
159
+ export { SWATCH_SIZE, facetSwatch, isColorAxis, swatchColor } from "./swatches.js";
160
+ export type { ColorAxisLike } from "./swatches.js";
161
+
154
162
  // ── the browse surfaces a storefront PLACES (this pair does not lay them
155
163
  // out: where a popular-values block or a partition row belongs on a
156
164
  // category page is the page's decision) ──────────────────────────────────
@@ -183,6 +191,7 @@ export type {
183
191
  FacetPanelPaneProps,
184
192
  CategoryFilterSlotProps,
185
193
  GeoFilterSlotProps,
194
+ SearchRailSurface,
186
195
  } from "./FacetPanelPane.js";
187
196
 
188
197
  export { RankingDisclosurePane, RANKING_MAX_WIDTH } from "./RankingDisclosurePane.js";
@@ -0,0 +1,214 @@
1
+ /**
2
+ * A COLOUR FACET SHOWS THE COLOUR.
3
+ *
4
+ * The reference's colour group draws a filled dot beside every value; ours
5
+ * drew the word and made the buyer read it (deep/elektronika-telefony.md §3,
6
+ * the one rail regression that pass found). A colour is the one attribute
7
+ * whose label is strictly worse than the thing itself — "silver" versus
8
+ * "gold" in a catalogue's own transliteration is a paragraph of prose for a difference a 10px dot settles.
9
+ *
10
+ * Two questions, and each is answered conservatively, because both failures
11
+ * are visible on a shopper's screen:
12
+ *
13
+ * 1. **is this axis a colour?** — {@link isColorAxis}, from the slug the
14
+ * catalogue mapped the axis to (and the address key, and `axis_role` if a
15
+ * schema ever carries one). A guess on the slug, exactly like
16
+ * `looksLikeExclusiveAxisSlug` next door: nothing on the wire marks an
17
+ * axis "colour", and inspecting the VALUES for colour-ish names would put
18
+ * dots on a paint-brand axis whose makes are called `Bordeaux`;
19
+ * 2. **which colour is this value?** — {@link swatchColor}, and the honest
20
+ * answer is usually "nobody said". A value code is a catalogue's own term
21
+ * (`chernyy`, `dark-slate-2`) and neither the answer, the feature schema
22
+ * nor the vocabulary endpoint carries a hue for it. So the pair paints a
23
+ * dot only where the code IS a colour by a name it can resolve — the
24
+ * design system's own colour roles first (§68: one neutral vocabulary of
25
+ * ROLES, and it deliberately ships no hue ramp), then CSS's own colour
26
+ * keywords, then a code that spells the hue out in hex — and draws
27
+ * NOTHING otherwise. An invented mapping from a transliterated Russian
28
+ * word to a hex value is data this pair does not have.
29
+ */
30
+ import { cssVar } from "@stapel/tokens";
31
+ import type { FeatureDef } from "@stapel/attributes-react";
32
+
33
+ /** What {@link isColorAxis} needs of a group — the shape `FacetGroup` has. */
34
+ export interface ColorAxisLike {
35
+ readonly slug: string;
36
+ readonly urlKey?: string;
37
+ readonly feature?: FeatureDef | undefined;
38
+ }
39
+
40
+ /**
41
+ * The control-type tails a catalogue hangs on an axis slug when one feature
42
+ * type is not enough to tell two mappings apart (`color_ref_select` is the
43
+ * live phones leaf's own colour axis; `color_select` and `color_multi` are
44
+ * the same axis under a different editor).
45
+ *
46
+ * Stripped before the head is read, so the control a value is PICKED with
47
+ * cannot change what the value IS.
48
+ */
49
+ const CONTROL_TAILS: readonly string[] = [
50
+ "ref_hierarchical_select",
51
+ "hierarchical_select",
52
+ "ref_select",
53
+ "multiselect",
54
+ "multi_select",
55
+ "select",
56
+ "multi",
57
+ "picker",
58
+ "field",
59
+ ];
60
+
61
+ function normalizeSlug(slug: string): string {
62
+ let normalized = slug.toLowerCase().replace(/-/g, "_");
63
+ for (const tail of CONTROL_TAILS) {
64
+ if (normalized.endsWith(`_${tail}`)) {
65
+ normalized = normalized.slice(0, -(tail.length + 1));
66
+ break;
67
+ }
68
+ }
69
+ return normalized;
70
+ }
71
+
72
+ /** The two spellings, and only as the slug's HEAD segment: `color_fridge` is
73
+ * the colour of a fridge and `colorado_region` is a place. */
74
+ const COLOR_HEADS: ReadonlySet<string> = new Set(["color", "colour"]);
75
+
76
+ function headIsColor(slug: string | undefined): boolean {
77
+ if (slug === undefined || slug === "") return false;
78
+ const normalized = normalizeSlug(slug);
79
+ const head = normalized.split("_")[0] ?? "";
80
+ return COLOR_HEADS.has(head);
81
+ }
82
+
83
+ /**
84
+ * Is this axis a colour vocabulary?
85
+ *
86
+ * Three sources, in the order of how much authority they carry:
87
+ *
88
+ * - `axis_role` — the schema SAYING what an axis is, which is the only
89
+ * non-guess available. The canon's role vocabulary is closed and has no
90
+ * colour in it yet (`make`/`model`/`generation`/`year`/`mileage`), so this
91
+ * arm reads the field as text and is dead until the canon grows one. It is
92
+ * written now so that the day it does, nothing here has to change;
93
+ * - the axis slug, and the ADDRESS key beside it — the live phones leaf maps
94
+ * its colour to `color_ref_select` and publishes it as `color`, so either
95
+ * spelling alone would miss half the deployments.
96
+ */
97
+ export function isColorAxis(group: ColorAxisLike): boolean {
98
+ const role = group.feature?.["axis_role"];
99
+ if (typeof role === "string" && COLOR_HEADS.has(role.toLowerCase())) return true;
100
+ return headIsColor(group.slug) || headIsColor(group.urlKey);
101
+ }
102
+
103
+ /**
104
+ * The design system's colour ROLES, for a value code that names one.
105
+ *
106
+ * The fleet's token vocabulary is neutral and role-shaped on purpose (§68):
107
+ * there is no `red`, no `blue`, and no ramp — so a colour axis whose values
108
+ * are hues matches nothing here, which is the correct answer rather than a
109
+ * gap. What does match is a catalogue that codes STATES as colours (a
110
+ * `status` axis mapped under a colour slug), and those take the brand's own
111
+ * value in both themes rather than a frozen hex.
112
+ */
113
+ const TOKEN_ROLE_SWATCHES: Readonly<Record<string, string>> = {
114
+ brand: cssVar("brand"),
115
+ error: cssVar("error"),
116
+ info: cssVar("info"),
117
+ link: cssVar("link"),
118
+ success: cssVar("success"),
119
+ surface: cssVar("surface"),
120
+ text: cssVar("text"),
121
+ };
122
+
123
+ /**
124
+ * CSS's own colour keywords — a NAME that matches, in the vocabulary every
125
+ * browser already agrees on.
126
+ *
127
+ * The basic sixteen plus the extended keywords a product catalogue actually
128
+ * uses. Deliberately not the full 148: every entry here is a promise that a
129
+ * value code spelled that way means that colour, and `rebeccapurple` in a
130
+ * phone catalogue is far likelier to be somebody's model name.
131
+ */
132
+ const CSS_COLOR_KEYWORDS: readonly string[] = [
133
+ "aqua",
134
+ "beige",
135
+ "black",
136
+ "blue",
137
+ "brown",
138
+ "chocolate",
139
+ "coral",
140
+ "crimson",
141
+ "cyan",
142
+ "fuchsia",
143
+ "gold",
144
+ "gray",
145
+ "green",
146
+ "grey",
147
+ "indigo",
148
+ "ivory",
149
+ "khaki",
150
+ "lavender",
151
+ "lime",
152
+ "magenta",
153
+ "maroon",
154
+ "navy",
155
+ "olive",
156
+ "orange",
157
+ "orchid",
158
+ "pink",
159
+ "plum",
160
+ "purple",
161
+ "red",
162
+ "salmon",
163
+ "sand",
164
+ "sienna",
165
+ "silver",
166
+ "skyblue",
167
+ "tan",
168
+ "teal",
169
+ "tomato",
170
+ "turquoise",
171
+ "violet",
172
+ "wheat",
173
+ "white",
174
+ "yellow",
175
+ ];
176
+
177
+ const CSS_COLOR_SET: ReadonlySet<string> = new Set(CSS_COLOR_KEYWORDS);
178
+
179
+ /** `#abc`, `#aabbcc`, `#aabbccdd` — a catalogue that codes the hue itself. */
180
+ const HEX = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
181
+
182
+ /**
183
+ * The CSS colour one value code names, or `null` for "nobody said".
184
+ *
185
+ * `null` is the ordinary answer and the row then draws no dot at all: a grey
186
+ * placeholder beside eleven values would say "these are all the same colour",
187
+ * which is worse than the word on its own.
188
+ *
189
+ * The code is read as written apart from case and separators — `dark_blue`
190
+ * and `dark-blue` are one code and neither is a CSS keyword, so both get
191
+ * nothing. Only a code that IS a name resolves.
192
+ */
193
+ export function swatchColor(code: string): string | null {
194
+ const raw = code.trim();
195
+ if (raw === "") return null;
196
+ if (HEX.test(raw)) return raw;
197
+ const normalized = raw.toLowerCase().replace(/[-_\s]/g, "");
198
+ const role = TOKEN_ROLE_SWATCHES[normalized];
199
+ if (role !== undefined) return role;
200
+ return CSS_COLOR_SET.has(normalized) ? normalized : null;
201
+ }
202
+
203
+ /**
204
+ * The dot a value gets when this axis is a colour AND the value names one.
205
+ * `null` everywhere else, which is most of the time — see {@link swatchColor}.
206
+ */
207
+ export function facetSwatch(group: ColorAxisLike, code: string): string | null {
208
+ return isColorAxis(group) ? swatchColor(code) : null;
209
+ }
210
+
211
+ /** The swatch's own size, in CSS pixels: a dot beside a line of text, sized
212
+ * to the x-height rather than to the control, so it reads as part of the
213
+ * label and not as a second checkbox. */
214
+ export const SWATCH_SIZE = 12;