@stapel/search-react 0.21.0 → 0.23.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 (86) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/MODULE.md +12 -0
  3. package/README.md +143 -2
  4. package/dist/api/types.d.ts +37 -3
  5. package/dist/api/types.d.ts.map +1 -1
  6. package/dist/api/types.js.map +1 -1
  7. package/dist/default/FacetGroupControl.d.ts +53 -5
  8. package/dist/default/FacetGroupControl.d.ts.map +1 -1
  9. package/dist/default/FacetGroupControl.js +189 -8
  10. package/dist/default/FacetGroupControl.js.map +1 -1
  11. package/dist/default/FacetPanelPane.d.ts +41 -0
  12. package/dist/default/FacetPanelPane.d.ts.map +1 -1
  13. package/dist/default/FacetPanelPane.js +72 -18
  14. package/dist/default/FacetPanelPane.js.map +1 -1
  15. package/dist/default/FilterChips.d.ts.map +1 -1
  16. package/dist/default/FilterChips.js +2 -2
  17. package/dist/default/FilterChips.js.map +1 -1
  18. package/dist/default/PartitionChips.d.ts +37 -0
  19. package/dist/default/PartitionChips.d.ts.map +1 -0
  20. package/dist/default/PartitionChips.js +120 -0
  21. package/dist/default/PartitionChips.js.map +1 -0
  22. package/dist/default/PopularValues.d.ts +58 -0
  23. package/dist/default/PopularValues.d.ts.map +1 -0
  24. package/dist/default/PopularValues.js +46 -0
  25. package/dist/default/PopularValues.js.map +1 -0
  26. package/dist/default/RangeFilterRow.d.ts.map +1 -1
  27. package/dist/default/RangeFilterRow.js +65 -11
  28. package/dist/default/RangeFilterRow.js.map +1 -1
  29. package/dist/default/SearchPage.d.ts +49 -0
  30. package/dist/default/SearchPage.d.ts.map +1 -1
  31. package/dist/default/SearchPage.js +43 -2
  32. package/dist/default/SearchPage.js.map +1 -1
  33. package/dist/default/index.d.ts +7 -3
  34. package/dist/default/index.d.ts.map +1 -1
  35. package/dist/default/index.js +8 -3
  36. package/dist/default/index.js.map +1 -1
  37. package/dist/headless/useFacetLabels.js +7 -7
  38. package/dist/headless/useFacetLabels.js.map +1 -1
  39. package/dist/i18n/es.d.ts.map +1 -1
  40. package/dist/i18n/es.js +9 -0
  41. package/dist/i18n/es.js.map +1 -1
  42. package/dist/i18n/keys.d.ts +52 -0
  43. package/dist/i18n/keys.d.ts.map +1 -1
  44. package/dist/i18n/keys.js +62 -0
  45. package/dist/i18n/keys.js.map +1 -1
  46. package/dist/i18n/ru.d.ts.map +1 -1
  47. package/dist/i18n/ru.js +9 -0
  48. package/dist/i18n/ru.js.map +1 -1
  49. package/dist/index.d.ts +5 -4
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/state/facets.d.ts +90 -6
  54. package/dist/state/facets.d.ts.map +1 -1
  55. package/dist/state/facets.js +216 -15
  56. package/dist/state/facets.js.map +1 -1
  57. package/dist/state/ranges.d.ts +22 -0
  58. package/dist/state/ranges.d.ts.map +1 -1
  59. package/dist/state/ranges.js +30 -0
  60. package/dist/state/ranges.js.map +1 -1
  61. package/dist/state/translit.d.ts +58 -0
  62. package/dist/state/translit.d.ts.map +1 -0
  63. package/dist/state/translit.js +115 -0
  64. package/dist/state/translit.js.map +1 -0
  65. package/llms.txt +3 -1
  66. package/manifest.json +45 -1
  67. package/nav-manifest.json +1 -1
  68. package/package.json +8 -8
  69. package/src/analytics/generated/events.json +1 -1
  70. package/src/api/types.ts +42 -3
  71. package/src/default/FacetGroupControl.tsx +339 -9
  72. package/src/default/FacetPanelPane.tsx +131 -19
  73. package/src/default/FilterChips.tsx +5 -2
  74. package/src/default/PartitionChips.tsx +204 -0
  75. package/src/default/PopularValues.tsx +151 -0
  76. package/src/default/RangeFilterRow.tsx +116 -1
  77. package/src/default/SearchPage.tsx +61 -2
  78. package/src/default/index.ts +27 -2
  79. package/src/headless/useFacetLabels.ts +7 -7
  80. package/src/i18n/es.ts +9 -0
  81. package/src/i18n/keys.ts +63 -0
  82. package/src/i18n/ru.ts +9 -0
  83. package/src/index.ts +13 -0
  84. package/src/state/facets.ts +259 -27
  85. package/src/state/ranges.ts +44 -0
  86. package/src/state/translit.ts +113 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Matching a typed prefix against a dictionary value ACROSS ALPHABETS.
3
+ *
4
+ * A vocabulary of car makes holds `Toyota`, `Land Rover`, `Timberland` — Latin
5
+ * words, in a catalogue read by people typing Cyrillic. "toyota" spelled in
6
+ * Cyrillic and `Toyota` are the same word said in two scripts, and a
7
+ * `String.includes` over the caption answers "no" to every one of them. The
8
+ * autocatalog has 418 makes: without this, the search box in a dictionary
9
+ * facet is a box that works only for the half of the buyers whose keyboard
10
+ * matches the catalogue's.
11
+ *
12
+ * ── Two keys per word, and why the second one exists ──────────────────────
13
+ *
14
+ * The first key is a plain transliteration: Cyrillic → Latin through the table
15
+ * below, lowercased, everything but letters and digits dropped. That alone
16
+ * gets the Cyrillic spelling of "toyota" to `toiota` and `Toyota` to `toyota`
17
+ * — still not equal, and this is the general case rather than a bad table.
18
+ * Vowels are exactly where two scripts disagree: `i` against `y`, `e` against
19
+ * `a` in a borrowed word, `timberlend` against `timberland`. A vowel-perfect
20
+ * table does not exist, because the disagreement is in the borrowing, not in
21
+ * the letters.
22
+ *
23
+ * So the second key is the CONSONANT SKELETON: the first key with its vowels
24
+ * removed. `timberlend` and `timberland` are both `tmbrlnd`; `toiota` and
25
+ * `toyota` are both `tt`. Digits stay — a skeleton is about letters people
26
+ * disagree on, and `5` is not one.
27
+ *
28
+ * A candidate matches when either key of any of its WORDS starts with the
29
+ * corresponding key of the needle: a typed "rover" in either script finds
30
+ * `Land Rover`, and typing into the box narrows as you go, which is the whole
31
+ * point of a prefix rule. The skeleton arm is skipped for a needle that has no
32
+ * consonants at all — an empty skeleton is a prefix of everything, and a box
33
+ * that matches all 418 makes after one vowel is a box that has stopped
34
+ * answering.
35
+ *
36
+ * Table-driven and dependency-free on purpose: this is 40 pairs of characters,
37
+ * and a transliteration library is a page-weight decision made for a lookup
38
+ * table.
39
+ */
40
+
41
+ /**
42
+ * Cyrillic → Latin, chosen for how the two scripts spell the SAME borrowed
43
+ * name rather than for a standard's fidelity: `kha` is `h` (Honda, not
44
+ * Khonda), `tse` is `c`, the two signs are nothing. Any letter absent here is
45
+ * kept as it is, so a Latin caption passes through untouched. Keys are
46
+ * quoted — the fleet's source is English, and a Cyrillic identifier is not.
47
+ */
48
+ const CYRILLIC_TO_LATIN: Readonly<Record<string, string>> = {
49
+ "а": "a", "б": "b", "в": "v", "г": "g", "д": "d", "е": "e", "ё": "e",
50
+ "ж": "zh", "з": "z", "и": "i", "й": "i", "к": "k", "л": "l", "м": "m",
51
+ "н": "n", "о": "o", "п": "p", "р": "r", "с": "s", "т": "t", "у": "u",
52
+ "ф": "f", "х": "h", "ц": "c", "ч": "ch", "ш": "sh", "щ": "sh", "ъ": "",
53
+ "ы": "y", "ь": "", "э": "e", "ю": "u", "я": "a",
54
+ // Ukrainian/Belarusian letters a shared vocabulary can carry.
55
+ "і": "i", "ї": "i", "є": "e", "ґ": "g", "ў": "u",
56
+ };
57
+
58
+ /** Vowels of the transliterated form — `y` included, because it stands for a
59
+ * Cyrillic semi-vowel as often as it stands for a consonant. */
60
+ const VOWELS = new Set(["a", "e", "i", "o", "u", "y"]);
61
+
62
+ /**
63
+ * One word, transliterated and folded to `[a-z0-9]`.
64
+ *
65
+ * `NFKD` + combining-mark strip first, so `Škoda` and `Skoda` are one word
66
+ * before anything else looks at them.
67
+ */
68
+ export function translitKey(text: string): string {
69
+ const folded = text
70
+ .normalize("NFKD")
71
+ .replace(/[\u0300-\u036f]/g, "")
72
+ .toLowerCase();
73
+ let out = "";
74
+ for (const char of folded) {
75
+ const mapped = CYRILLIC_TO_LATIN[char];
76
+ if (mapped !== undefined) out += mapped;
77
+ else if (/[a-z0-9]/.test(char)) out += char;
78
+ }
79
+ return out;
80
+ }
81
+
82
+ /** {@link translitKey} without its vowels — the key two spellings of one
83
+ * borrowed name agree on. */
84
+ export function consonantKey(text: string): string {
85
+ let out = "";
86
+ for (const char of translitKey(text)) if (!VOWELS.has(char)) out += char;
87
+ return out;
88
+ }
89
+
90
+ /** The words of a caption, split on everything that is not a letter or a
91
+ * digit — so `Land Rover` is reachable by "rover" in either script. */
92
+ function words(text: string): readonly string[] {
93
+ return text.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length > 0);
94
+ }
95
+
96
+ /**
97
+ * Does `candidate` answer the typed `needle`?
98
+ *
99
+ * Prefix, not substring: a dictionary of makes searched by substring puts
100
+ * `Great Wall` under `all`. Every word of the candidate is a starting point,
101
+ * and both keys are tried — see the module note.
102
+ */
103
+ export function translitPrefixMatch(needle: string, candidate: string): boolean {
104
+ const key = translitKey(needle);
105
+ if (key.length === 0) return true;
106
+ const skeleton = consonantKey(needle);
107
+ for (const word of [candidate, ...words(candidate)]) {
108
+ const wordKey = translitKey(word);
109
+ if (wordKey.startsWith(key)) return true;
110
+ if (skeleton.length > 0 && consonantKey(word).startsWith(skeleton)) return true;
111
+ }
112
+ return false;
113
+ }