@ai-matrx/kit 0.5.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 (118) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -0
  4. package/dist/autosave.cjs +92 -0
  5. package/dist/autosave.cjs.map +1 -0
  6. package/dist/autosave.d.cts +17 -0
  7. package/dist/autosave.d.ts +17 -0
  8. package/dist/autosave.js +72 -0
  9. package/dist/autosave.js.map +1 -0
  10. package/dist/clipboard.cjs +152 -0
  11. package/dist/clipboard.cjs.map +1 -0
  12. package/dist/clipboard.d.cts +17 -0
  13. package/dist/clipboard.d.ts +17 -0
  14. package/dist/clipboard.js +132 -0
  15. package/dist/clipboard.js.map +1 -0
  16. package/dist/color-util.cjs +790 -0
  17. package/dist/color-util.cjs.map +1 -0
  18. package/dist/color-util.d.cts +235 -0
  19. package/dist/color-util.d.ts +235 -0
  20. package/dist/color-util.js +767 -0
  21. package/dist/color-util.js.map +1 -0
  22. package/dist/concurrency.cjs +59 -0
  23. package/dist/concurrency.cjs.map +1 -0
  24. package/dist/concurrency.d.cts +19 -0
  25. package/dist/concurrency.d.ts +19 -0
  26. package/dist/concurrency.js +38 -0
  27. package/dist/concurrency.js.map +1 -0
  28. package/dist/confirm.cjs +344 -0
  29. package/dist/confirm.cjs.map +1 -0
  30. package/dist/confirm.d.cts +132 -0
  31. package/dist/confirm.d.ts +132 -0
  32. package/dist/confirm.js +312 -0
  33. package/dist/confirm.js.map +1 -0
  34. package/dist/delimiter-guard.cjs +172 -0
  35. package/dist/delimiter-guard.cjs.map +1 -0
  36. package/dist/delimiter-guard.d.cts +119 -0
  37. package/dist/delimiter-guard.d.ts +119 -0
  38. package/dist/delimiter-guard.js +151 -0
  39. package/dist/delimiter-guard.js.map +1 -0
  40. package/dist/drafts.cjs +223 -0
  41. package/dist/drafts.cjs.map +1 -0
  42. package/dist/drafts.d.cts +62 -0
  43. package/dist/drafts.d.ts +62 -0
  44. package/dist/drafts.js +201 -0
  45. package/dist/drafts.js.map +1 -0
  46. package/dist/idb-store.cjs +178 -0
  47. package/dist/idb-store.cjs.map +1 -0
  48. package/dist/idb-store.d.cts +111 -0
  49. package/dist/idb-store.d.ts +111 -0
  50. package/dist/idb-store.js +155 -0
  51. package/dist/idb-store.js.map +1 -0
  52. package/dist/idle-scheduler.cjs +268 -0
  53. package/dist/idle-scheduler.cjs.map +1 -0
  54. package/dist/idle-scheduler.d.cts +185 -0
  55. package/dist/idle-scheduler.d.ts +185 -0
  56. package/dist/idle-scheduler.js +246 -0
  57. package/dist/idle-scheduler.js.map +1 -0
  58. package/dist/index.cjs +3113 -0
  59. package/dist/index.cjs.map +1 -0
  60. package/dist/index.d.cts +897 -0
  61. package/dist/index.d.ts +897 -0
  62. package/dist/index.js +3088 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/invalidation.cjs +64 -0
  65. package/dist/invalidation.cjs.map +1 -0
  66. package/dist/invalidation.d.cts +52 -0
  67. package/dist/invalidation.d.ts +52 -0
  68. package/dist/invalidation.js +43 -0
  69. package/dist/invalidation.js.map +1 -0
  70. package/dist/json-format.cjs +319 -0
  71. package/dist/json-format.cjs.map +1 -0
  72. package/dist/json-format.d.cts +148 -0
  73. package/dist/json-format.d.ts +148 -0
  74. package/dist/json-format.js +286 -0
  75. package/dist/json-format.js.map +1 -0
  76. package/dist/latest-request.cjs +36 -0
  77. package/dist/latest-request.cjs.map +1 -0
  78. package/dist/latest-request.d.cts +29 -0
  79. package/dist/latest-request.d.ts +29 -0
  80. package/dist/latest-request.js +16 -0
  81. package/dist/latest-request.js.map +1 -0
  82. package/dist/qr.cjs +109 -0
  83. package/dist/qr.cjs.map +1 -0
  84. package/dist/qr.d.cts +47 -0
  85. package/dist/qr.d.ts +47 -0
  86. package/dist/qr.js +78 -0
  87. package/dist/qr.js.map +1 -0
  88. package/dist/react-tree.cjs +69 -0
  89. package/dist/react-tree.cjs.map +1 -0
  90. package/dist/react-tree.d.cts +38 -0
  91. package/dist/react-tree.d.ts +38 -0
  92. package/dist/react-tree.js +38 -0
  93. package/dist/react-tree.js.map +1 -0
  94. package/dist/search-scoring.cjs +116 -0
  95. package/dist/search-scoring.cjs.map +1 -0
  96. package/dist/search-scoring.d.cts +77 -0
  97. package/dist/search-scoring.d.ts +77 -0
  98. package/dist/search-scoring.js +95 -0
  99. package/dist/search-scoring.js.map +1 -0
  100. package/dist/text-case.cjs +313 -0
  101. package/dist/text-case.cjs.map +1 -0
  102. package/dist/text-case.d.cts +53 -0
  103. package/dist/text-case.d.ts +53 -0
  104. package/dist/text-case.js +292 -0
  105. package/dist/text-case.js.map +1 -0
  106. package/dist/toast.cjs +74 -0
  107. package/dist/toast.cjs.map +1 -0
  108. package/dist/toast.d.cts +92 -0
  109. package/dist/toast.d.ts +92 -0
  110. package/dist/toast.js +54 -0
  111. package/dist/toast.js.map +1 -0
  112. package/dist/url-state.cjs +212 -0
  113. package/dist/url-state.cjs.map +1 -0
  114. package/dist/url-state.d.cts +90 -0
  115. package/dist/url-state.d.ts +90 -0
  116. package/dist/url-state.js +199 -0
  117. package/dist/url-state.js.map +1 -0
  118. package/package.json +286 -0
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/search-scoring.ts
21
+ var search_scoring_exports = {};
22
+ __export(search_scoring_exports, {
23
+ computeSearchScore: () => computeSearchScore,
24
+ filterAndSortBySearch: () => filterAndSortBySearch,
25
+ idMatchesQuery: () => idMatchesQuery,
26
+ matchesSearch: () => matchesSearch
27
+ });
28
+ module.exports = __toCommonJS(search_scoring_exports);
29
+ var WEIGHT_TABLE = {
30
+ title: { exact: 1e4, startsWith: 5e3, includes: 2e3 },
31
+ subtitle: { exact: 2e3, startsWith: 1e3, includes: 500 },
32
+ body: { exact: 1e3, startsWith: 600, includes: 400 },
33
+ tag: { exact: 500, startsWith: 400, includes: 300 },
34
+ meta: { exact: 200, startsWith: 150, includes: 100 },
35
+ id: { exact: 100, startsWith: 75, includes: 50 }
36
+ };
37
+ var MIN_AUTO_ID_QUERY_LEN = 3;
38
+ function getStringId(item) {
39
+ if (item && typeof item === "object" && "id" in item) {
40
+ const id = item.id;
41
+ if (typeof id === "string" && id.length > 0) return id;
42
+ }
43
+ return null;
44
+ }
45
+ function resolveTiers(field) {
46
+ if (field.weight === "custom" || field.exact != null) {
47
+ return {
48
+ exact: field.exact ?? 0,
49
+ startsWith: field.startsWith ?? 0,
50
+ includes: field.includes ?? 0
51
+ };
52
+ }
53
+ return WEIGHT_TABLE[field.weight ?? "body"];
54
+ }
55
+ function scoreValue(value, q, tiers) {
56
+ if (!value) return 0;
57
+ const v = value.toLowerCase();
58
+ if (v === q) return tiers.exact;
59
+ if (v.startsWith(q)) return tiers.startsWith;
60
+ if (v.includes(q)) return tiers.includes;
61
+ return 0;
62
+ }
63
+ function computeSearchScore(item, query, fields) {
64
+ const trimmed = query.trim();
65
+ if (!trimmed) return 0;
66
+ const q = trimmed.toLowerCase();
67
+ let total = 0;
68
+ fields.forEach((field, idx) => {
69
+ const raw = field.get(item);
70
+ if (raw == null) return;
71
+ const tiers = resolveTiers(field);
72
+ let best = 0;
73
+ if (Array.isArray(raw)) {
74
+ for (const v of raw) {
75
+ if (typeof v !== "string") continue;
76
+ const s = scoreValue(v, q, tiers);
77
+ if (s > best) best = s;
78
+ }
79
+ } else if (typeof raw === "string") {
80
+ best = scoreValue(raw, q, tiers);
81
+ }
82
+ if (best > 0) {
83
+ total += best + (fields.length - idx);
84
+ }
85
+ });
86
+ const hasExplicitId = fields.some((f) => f.weight === "id");
87
+ if (!hasExplicitId && q.length >= MIN_AUTO_ID_QUERY_LEN) {
88
+ const id = getStringId(item);
89
+ if (id) {
90
+ total += scoreValue(id, q, WEIGHT_TABLE.id);
91
+ }
92
+ }
93
+ return total;
94
+ }
95
+ function matchesSearch(item, query, fields) {
96
+ return computeSearchScore(item, query, fields) > 0;
97
+ }
98
+ function idMatchesQuery(item, query) {
99
+ const q = query.trim().toLowerCase();
100
+ if (q.length < MIN_AUTO_ID_QUERY_LEN) return false;
101
+ const id = getStringId(item);
102
+ return id != null && id.toLowerCase().includes(q);
103
+ }
104
+ function filterAndSortBySearch(items, query, fields) {
105
+ const trimmed = query.trim();
106
+ if (!trimmed) return items.slice();
107
+ const scored = [];
108
+ for (let i = 0; i < items.length; i++) {
109
+ const item = items[i];
110
+ const score = computeSearchScore(item, trimmed, fields);
111
+ if (score > 0) scored.push({ item, score, idx: i });
112
+ }
113
+ scored.sort((a, b) => b.score - a.score || a.idx - b.idx);
114
+ return scored.map((s) => s.item);
115
+ }
116
+ //# sourceMappingURL=search-scoring.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/search-scoring.ts"],"sourcesContent":["/**\n * Relevance-weighted search scoring.\n *\n * Use instead of the naive `name.includes(q) || description.includes(q)` pattern\n * so that title/name matches rank above description matches, and exact/prefix\n * matches rank above partial ones.\n *\n * ── Quick start ────────────────────────────────────────────────────────────────\n * const filtered = filterAndSortBySearch(items, query, [\n * { get: (t) => t.name, weight: \"title\" },\n * { get: (t) => t.description, weight: \"body\" },\n * { get: (t) => t.tags, weight: \"tag\" },\n * ]);\n *\n * ── Weight tiers (higher = more important field) ──────────────────────────────\n * title — the primary identifier (name, label, subject)\n * subtitle — secondary identifier (vendor, author, category name)\n * body — long-form descriptive text (description, summary)\n * tag — tag/category labels\n * meta — weak metadata (modelId, type)\n * id — raw identifiers (uuid, slug) — only useful for pasted-id lookups\n *\n * Within each field, an EXACT match > STARTS-WITH match > INCLUDES match.\n * Fields declared first are a slight tiebreaker (via field-index bonus).\n *\n * ── Automatic id matching ─────────────────────────────────────────────────────\n * Every item with a string `id` is ALSO matched against the query at the `id`\n * weight tier, automatically — you do NOT need to declare an id field. This\n * means a user can paste a full or partial UUID into ANY search box wired to\n * this helper and find the record. It kicks in from {@link MIN_AUTO_ID_QUERY_LEN}\n * characters up (so short queries don't match random hex). Declare an explicit\n * `{ weight: \"id\" }` field only if you want id matching at any length / on a\n * non-`id` property; doing so opts that callsite out of the automatic pass.\n */\n\nexport type SearchFieldWeight =\n | \"title\"\n | \"subtitle\"\n | \"body\"\n | \"tag\"\n | \"meta\"\n | \"id\"\n | \"custom\";\n\nexport interface SearchFieldConfig<T> {\n /**\n * Extracts the value(s) from the item. Return a string, an array of strings,\n * or null/undefined. Arrays score based on the best-matching element.\n */\n get: (item: T) => string | string[] | null | undefined;\n /** Field importance tier. Defaults to \"body\". */\n weight?: SearchFieldWeight;\n /** Optional override for custom tiers. Ignored when `weight` is preset. */\n exact?: number;\n startsWith?: number;\n includes?: number;\n}\n\nconst WEIGHT_TABLE: Record<\n Exclude<SearchFieldWeight, \"custom\">,\n { exact: number; startsWith: number; includes: number }\n> = {\n title: { exact: 10000, startsWith: 5000, includes: 2000 },\n subtitle: { exact: 2000, startsWith: 1000, includes: 500 },\n body: { exact: 1000, startsWith: 600, includes: 400 },\n tag: { exact: 500, startsWith: 400, includes: 300 },\n meta: { exact: 200, startsWith: 150, includes: 100 },\n id: { exact: 100, startsWith: 75, includes: 50 },\n};\n\n/**\n * Below this query length we do NOT auto-match the row `id`. A 1–2 char query\n * is almost always a substring of *some* hex chars in *every* UUID, so matching\n * id at that length would flood results with the whole table. From 3 chars up a\n * partial-UUID paste is selective enough to be a real lookup.\n */\nconst MIN_AUTO_ID_QUERY_LEN = 3;\n\n/** Pull a non-empty string `id` off an item, or null if it has none. */\nfunction getStringId(item: unknown): string | null {\n if (item && typeof item === \"object\" && \"id\" in item) {\n const id = (item as { id?: unknown }).id;\n if (typeof id === \"string\" && id.length > 0) return id;\n }\n return null;\n}\n\nfunction resolveTiers(field: SearchFieldConfig<unknown>) {\n if (field.weight === \"custom\" || field.exact != null) {\n return {\n exact: field.exact ?? 0,\n startsWith: field.startsWith ?? 0,\n includes: field.includes ?? 0,\n };\n }\n return WEIGHT_TABLE[field.weight ?? \"body\"];\n}\n\nfunction scoreValue(\n value: string,\n q: string,\n tiers: { exact: number; startsWith: number; includes: number },\n): number {\n if (!value) return 0;\n const v = value.toLowerCase();\n if (v === q) return tiers.exact;\n if (v.startsWith(q)) return tiers.startsWith;\n if (v.includes(q)) return tiers.includes;\n return 0;\n}\n\n/**\n * Compute a weighted relevance score for `item` against `query`.\n * Returns 0 if there is no match — callers can treat `> 0` as a match predicate.\n *\n * Within each field, multiple values (e.g. tags) contribute the BEST match,\n * not the sum, so an item with many tags doesn't unfairly outrank one with a\n * single exact title match.\n */\nexport function computeSearchScore<T>(\n item: T,\n query: string,\n fields: SearchFieldConfig<T>[],\n): number {\n const trimmed = query.trim();\n if (!trimmed) return 0;\n const q = trimmed.toLowerCase();\n\n let total = 0;\n fields.forEach((field, idx) => {\n const raw = field.get(item);\n if (raw == null) return;\n const tiers = resolveTiers(field as SearchFieldConfig<unknown>);\n\n let best = 0;\n if (Array.isArray(raw)) {\n for (const v of raw) {\n if (typeof v !== \"string\") continue;\n const s = scoreValue(v, q, tiers);\n if (s > best) best = s;\n }\n } else if (typeof raw === \"string\") {\n best = scoreValue(raw, q, tiers);\n }\n\n if (best > 0) {\n // Tiny bias so that when two fields tie, the one declared first wins.\n total += best + (fields.length - idx);\n }\n });\n\n // Auto-match the row's UUID `id` against EVERY search box. A user can paste a\n // full or partial id and find the record, without each callsite remembering\n // to declare an id field. Skipped when the caller already declared an\n // explicit `weight: \"id\"` field (so we don't double-score), and gated on a\n // minimum query length so short queries don't match random hex substrings.\n const hasExplicitId = fields.some((f) => f.weight === \"id\");\n if (!hasExplicitId && q.length >= MIN_AUTO_ID_QUERY_LEN) {\n const id = getStringId(item);\n if (id) {\n total += scoreValue(id, q, WEIGHT_TABLE.id);\n }\n }\n\n return total;\n}\n\nexport function matchesSearch<T>(\n item: T,\n query: string,\n fields: SearchFieldConfig<T>[],\n): boolean {\n return computeSearchScore(item, query, fields) > 0;\n}\n\n/**\n * Drop-in id-match for hand-rolled `.filter()` predicates that can't (yet) move\n * onto {@link filterAndSortBySearch}. Returns true when `query` is a substring\n * of the item's string `id`, applying the same {@link MIN_AUTO_ID_QUERY_LEN}\n * guard as the automatic pass so short queries don't match random hex.\n *\n * list.filter((x) => x.name.toLowerCase().includes(q) || idMatchesQuery(x, q))\n *\n * Prefer migrating the callsite to `filterAndSortBySearch` (which does this for\n * free); reach for this only when an existing custom sort must be preserved.\n */\nexport function idMatchesQuery(item: unknown, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (q.length < MIN_AUTO_ID_QUERY_LEN) return false;\n const id = getStringId(item);\n return id != null && id.toLowerCase().includes(q);\n}\n\n/**\n * Filter out non-matches and sort remaining items by descending relevance.\n * Stable with respect to the original order when two items tie.\n */\nexport function filterAndSortBySearch<T>(\n items: readonly T[],\n query: string,\n fields: SearchFieldConfig<T>[],\n): T[] {\n const trimmed = query.trim();\n if (!trimmed) return items.slice();\n\n const scored: { item: T; score: number; idx: number }[] = [];\n for (let i = 0; i < items.length; i++) {\n const item = items[i] as T;\n const score = computeSearchScore(item, trimmed, fields);\n if (score > 0) scored.push({ item, score, idx: i });\n }\n scored.sort((a, b) => (b.score - a.score) || (a.idx - b.idx));\n return scored.map((s) => s.item);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Relevance-weighted search scoring.
3
+ *
4
+ * Use instead of the naive `name.includes(q) || description.includes(q)` pattern
5
+ * so that title/name matches rank above description matches, and exact/prefix
6
+ * matches rank above partial ones.
7
+ *
8
+ * ── Quick start ────────────────────────────────────────────────────────────────
9
+ * const filtered = filterAndSortBySearch(items, query, [
10
+ * { get: (t) => t.name, weight: "title" },
11
+ * { get: (t) => t.description, weight: "body" },
12
+ * { get: (t) => t.tags, weight: "tag" },
13
+ * ]);
14
+ *
15
+ * ── Weight tiers (higher = more important field) ──────────────────────────────
16
+ * title — the primary identifier (name, label, subject)
17
+ * subtitle — secondary identifier (vendor, author, category name)
18
+ * body — long-form descriptive text (description, summary)
19
+ * tag — tag/category labels
20
+ * meta — weak metadata (modelId, type)
21
+ * id — raw identifiers (uuid, slug) — only useful for pasted-id lookups
22
+ *
23
+ * Within each field, an EXACT match > STARTS-WITH match > INCLUDES match.
24
+ * Fields declared first are a slight tiebreaker (via field-index bonus).
25
+ *
26
+ * ── Automatic id matching ─────────────────────────────────────────────────────
27
+ * Every item with a string `id` is ALSO matched against the query at the `id`
28
+ * weight tier, automatically — you do NOT need to declare an id field. This
29
+ * means a user can paste a full or partial UUID into ANY search box wired to
30
+ * this helper and find the record. It kicks in from {@link MIN_AUTO_ID_QUERY_LEN}
31
+ * characters up (so short queries don't match random hex). Declare an explicit
32
+ * `{ weight: "id" }` field only if you want id matching at any length / on a
33
+ * non-`id` property; doing so opts that callsite out of the automatic pass.
34
+ */
35
+ type SearchFieldWeight = "title" | "subtitle" | "body" | "tag" | "meta" | "id" | "custom";
36
+ interface SearchFieldConfig<T> {
37
+ /**
38
+ * Extracts the value(s) from the item. Return a string, an array of strings,
39
+ * or null/undefined. Arrays score based on the best-matching element.
40
+ */
41
+ get: (item: T) => string | string[] | null | undefined;
42
+ /** Field importance tier. Defaults to "body". */
43
+ weight?: SearchFieldWeight;
44
+ /** Optional override for custom tiers. Ignored when `weight` is preset. */
45
+ exact?: number;
46
+ startsWith?: number;
47
+ includes?: number;
48
+ }
49
+ /**
50
+ * Compute a weighted relevance score for `item` against `query`.
51
+ * Returns 0 if there is no match — callers can treat `> 0` as a match predicate.
52
+ *
53
+ * Within each field, multiple values (e.g. tags) contribute the BEST match,
54
+ * not the sum, so an item with many tags doesn't unfairly outrank one with a
55
+ * single exact title match.
56
+ */
57
+ declare function computeSearchScore<T>(item: T, query: string, fields: SearchFieldConfig<T>[]): number;
58
+ declare function matchesSearch<T>(item: T, query: string, fields: SearchFieldConfig<T>[]): boolean;
59
+ /**
60
+ * Drop-in id-match for hand-rolled `.filter()` predicates that can't (yet) move
61
+ * onto {@link filterAndSortBySearch}. Returns true when `query` is a substring
62
+ * of the item's string `id`, applying the same {@link MIN_AUTO_ID_QUERY_LEN}
63
+ * guard as the automatic pass so short queries don't match random hex.
64
+ *
65
+ * list.filter((x) => x.name.toLowerCase().includes(q) || idMatchesQuery(x, q))
66
+ *
67
+ * Prefer migrating the callsite to `filterAndSortBySearch` (which does this for
68
+ * free); reach for this only when an existing custom sort must be preserved.
69
+ */
70
+ declare function idMatchesQuery(item: unknown, query: string): boolean;
71
+ /**
72
+ * Filter out non-matches and sort remaining items by descending relevance.
73
+ * Stable with respect to the original order when two items tie.
74
+ */
75
+ declare function filterAndSortBySearch<T>(items: readonly T[], query: string, fields: SearchFieldConfig<T>[]): T[];
76
+
77
+ export { type SearchFieldConfig, type SearchFieldWeight, computeSearchScore, filterAndSortBySearch, idMatchesQuery, matchesSearch };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Relevance-weighted search scoring.
3
+ *
4
+ * Use instead of the naive `name.includes(q) || description.includes(q)` pattern
5
+ * so that title/name matches rank above description matches, and exact/prefix
6
+ * matches rank above partial ones.
7
+ *
8
+ * ── Quick start ────────────────────────────────────────────────────────────────
9
+ * const filtered = filterAndSortBySearch(items, query, [
10
+ * { get: (t) => t.name, weight: "title" },
11
+ * { get: (t) => t.description, weight: "body" },
12
+ * { get: (t) => t.tags, weight: "tag" },
13
+ * ]);
14
+ *
15
+ * ── Weight tiers (higher = more important field) ──────────────────────────────
16
+ * title — the primary identifier (name, label, subject)
17
+ * subtitle — secondary identifier (vendor, author, category name)
18
+ * body — long-form descriptive text (description, summary)
19
+ * tag — tag/category labels
20
+ * meta — weak metadata (modelId, type)
21
+ * id — raw identifiers (uuid, slug) — only useful for pasted-id lookups
22
+ *
23
+ * Within each field, an EXACT match > STARTS-WITH match > INCLUDES match.
24
+ * Fields declared first are a slight tiebreaker (via field-index bonus).
25
+ *
26
+ * ── Automatic id matching ─────────────────────────────────────────────────────
27
+ * Every item with a string `id` is ALSO matched against the query at the `id`
28
+ * weight tier, automatically — you do NOT need to declare an id field. This
29
+ * means a user can paste a full or partial UUID into ANY search box wired to
30
+ * this helper and find the record. It kicks in from {@link MIN_AUTO_ID_QUERY_LEN}
31
+ * characters up (so short queries don't match random hex). Declare an explicit
32
+ * `{ weight: "id" }` field only if you want id matching at any length / on a
33
+ * non-`id` property; doing so opts that callsite out of the automatic pass.
34
+ */
35
+ type SearchFieldWeight = "title" | "subtitle" | "body" | "tag" | "meta" | "id" | "custom";
36
+ interface SearchFieldConfig<T> {
37
+ /**
38
+ * Extracts the value(s) from the item. Return a string, an array of strings,
39
+ * or null/undefined. Arrays score based on the best-matching element.
40
+ */
41
+ get: (item: T) => string | string[] | null | undefined;
42
+ /** Field importance tier. Defaults to "body". */
43
+ weight?: SearchFieldWeight;
44
+ /** Optional override for custom tiers. Ignored when `weight` is preset. */
45
+ exact?: number;
46
+ startsWith?: number;
47
+ includes?: number;
48
+ }
49
+ /**
50
+ * Compute a weighted relevance score for `item` against `query`.
51
+ * Returns 0 if there is no match — callers can treat `> 0` as a match predicate.
52
+ *
53
+ * Within each field, multiple values (e.g. tags) contribute the BEST match,
54
+ * not the sum, so an item with many tags doesn't unfairly outrank one with a
55
+ * single exact title match.
56
+ */
57
+ declare function computeSearchScore<T>(item: T, query: string, fields: SearchFieldConfig<T>[]): number;
58
+ declare function matchesSearch<T>(item: T, query: string, fields: SearchFieldConfig<T>[]): boolean;
59
+ /**
60
+ * Drop-in id-match for hand-rolled `.filter()` predicates that can't (yet) move
61
+ * onto {@link filterAndSortBySearch}. Returns true when `query` is a substring
62
+ * of the item's string `id`, applying the same {@link MIN_AUTO_ID_QUERY_LEN}
63
+ * guard as the automatic pass so short queries don't match random hex.
64
+ *
65
+ * list.filter((x) => x.name.toLowerCase().includes(q) || idMatchesQuery(x, q))
66
+ *
67
+ * Prefer migrating the callsite to `filterAndSortBySearch` (which does this for
68
+ * free); reach for this only when an existing custom sort must be preserved.
69
+ */
70
+ declare function idMatchesQuery(item: unknown, query: string): boolean;
71
+ /**
72
+ * Filter out non-matches and sort remaining items by descending relevance.
73
+ * Stable with respect to the original order when two items tie.
74
+ */
75
+ declare function filterAndSortBySearch<T>(items: readonly T[], query: string, fields: SearchFieldConfig<T>[]): T[];
76
+
77
+ export { type SearchFieldConfig, type SearchFieldWeight, computeSearchScore, filterAndSortBySearch, idMatchesQuery, matchesSearch };
@@ -0,0 +1,95 @@
1
+ // src/search-scoring.ts
2
+ var WEIGHT_TABLE = {
3
+ title: { exact: 1e4, startsWith: 5e3, includes: 2e3 },
4
+ subtitle: { exact: 2e3, startsWith: 1e3, includes: 500 },
5
+ body: { exact: 1e3, startsWith: 600, includes: 400 },
6
+ tag: { exact: 500, startsWith: 400, includes: 300 },
7
+ meta: { exact: 200, startsWith: 150, includes: 100 },
8
+ id: { exact: 100, startsWith: 75, includes: 50 }
9
+ };
10
+ var MIN_AUTO_ID_QUERY_LEN = 3;
11
+ function getStringId(item) {
12
+ if (item && typeof item === "object" && "id" in item) {
13
+ const id = item.id;
14
+ if (typeof id === "string" && id.length > 0) return id;
15
+ }
16
+ return null;
17
+ }
18
+ function resolveTiers(field) {
19
+ if (field.weight === "custom" || field.exact != null) {
20
+ return {
21
+ exact: field.exact ?? 0,
22
+ startsWith: field.startsWith ?? 0,
23
+ includes: field.includes ?? 0
24
+ };
25
+ }
26
+ return WEIGHT_TABLE[field.weight ?? "body"];
27
+ }
28
+ function scoreValue(value, q, tiers) {
29
+ if (!value) return 0;
30
+ const v = value.toLowerCase();
31
+ if (v === q) return tiers.exact;
32
+ if (v.startsWith(q)) return tiers.startsWith;
33
+ if (v.includes(q)) return tiers.includes;
34
+ return 0;
35
+ }
36
+ function computeSearchScore(item, query, fields) {
37
+ const trimmed = query.trim();
38
+ if (!trimmed) return 0;
39
+ const q = trimmed.toLowerCase();
40
+ let total = 0;
41
+ fields.forEach((field, idx) => {
42
+ const raw = field.get(item);
43
+ if (raw == null) return;
44
+ const tiers = resolveTiers(field);
45
+ let best = 0;
46
+ if (Array.isArray(raw)) {
47
+ for (const v of raw) {
48
+ if (typeof v !== "string") continue;
49
+ const s = scoreValue(v, q, tiers);
50
+ if (s > best) best = s;
51
+ }
52
+ } else if (typeof raw === "string") {
53
+ best = scoreValue(raw, q, tiers);
54
+ }
55
+ if (best > 0) {
56
+ total += best + (fields.length - idx);
57
+ }
58
+ });
59
+ const hasExplicitId = fields.some((f) => f.weight === "id");
60
+ if (!hasExplicitId && q.length >= MIN_AUTO_ID_QUERY_LEN) {
61
+ const id = getStringId(item);
62
+ if (id) {
63
+ total += scoreValue(id, q, WEIGHT_TABLE.id);
64
+ }
65
+ }
66
+ return total;
67
+ }
68
+ function matchesSearch(item, query, fields) {
69
+ return computeSearchScore(item, query, fields) > 0;
70
+ }
71
+ function idMatchesQuery(item, query) {
72
+ const q = query.trim().toLowerCase();
73
+ if (q.length < MIN_AUTO_ID_QUERY_LEN) return false;
74
+ const id = getStringId(item);
75
+ return id != null && id.toLowerCase().includes(q);
76
+ }
77
+ function filterAndSortBySearch(items, query, fields) {
78
+ const trimmed = query.trim();
79
+ if (!trimmed) return items.slice();
80
+ const scored = [];
81
+ for (let i = 0; i < items.length; i++) {
82
+ const item = items[i];
83
+ const score = computeSearchScore(item, trimmed, fields);
84
+ if (score > 0) scored.push({ item, score, idx: i });
85
+ }
86
+ scored.sort((a, b) => b.score - a.score || a.idx - b.idx);
87
+ return scored.map((s) => s.item);
88
+ }
89
+ export {
90
+ computeSearchScore,
91
+ filterAndSortBySearch,
92
+ idMatchesQuery,
93
+ matchesSearch
94
+ };
95
+ //# sourceMappingURL=search-scoring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/search-scoring.ts"],"sourcesContent":["/**\n * Relevance-weighted search scoring.\n *\n * Use instead of the naive `name.includes(q) || description.includes(q)` pattern\n * so that title/name matches rank above description matches, and exact/prefix\n * matches rank above partial ones.\n *\n * ── Quick start ────────────────────────────────────────────────────────────────\n * const filtered = filterAndSortBySearch(items, query, [\n * { get: (t) => t.name, weight: \"title\" },\n * { get: (t) => t.description, weight: \"body\" },\n * { get: (t) => t.tags, weight: \"tag\" },\n * ]);\n *\n * ── Weight tiers (higher = more important field) ──────────────────────────────\n * title — the primary identifier (name, label, subject)\n * subtitle — secondary identifier (vendor, author, category name)\n * body — long-form descriptive text (description, summary)\n * tag — tag/category labels\n * meta — weak metadata (modelId, type)\n * id — raw identifiers (uuid, slug) — only useful for pasted-id lookups\n *\n * Within each field, an EXACT match > STARTS-WITH match > INCLUDES match.\n * Fields declared first are a slight tiebreaker (via field-index bonus).\n *\n * ── Automatic id matching ─────────────────────────────────────────────────────\n * Every item with a string `id` is ALSO matched against the query at the `id`\n * weight tier, automatically — you do NOT need to declare an id field. This\n * means a user can paste a full or partial UUID into ANY search box wired to\n * this helper and find the record. It kicks in from {@link MIN_AUTO_ID_QUERY_LEN}\n * characters up (so short queries don't match random hex). Declare an explicit\n * `{ weight: \"id\" }` field only if you want id matching at any length / on a\n * non-`id` property; doing so opts that callsite out of the automatic pass.\n */\n\nexport type SearchFieldWeight =\n | \"title\"\n | \"subtitle\"\n | \"body\"\n | \"tag\"\n | \"meta\"\n | \"id\"\n | \"custom\";\n\nexport interface SearchFieldConfig<T> {\n /**\n * Extracts the value(s) from the item. Return a string, an array of strings,\n * or null/undefined. Arrays score based on the best-matching element.\n */\n get: (item: T) => string | string[] | null | undefined;\n /** Field importance tier. Defaults to \"body\". */\n weight?: SearchFieldWeight;\n /** Optional override for custom tiers. Ignored when `weight` is preset. */\n exact?: number;\n startsWith?: number;\n includes?: number;\n}\n\nconst WEIGHT_TABLE: Record<\n Exclude<SearchFieldWeight, \"custom\">,\n { exact: number; startsWith: number; includes: number }\n> = {\n title: { exact: 10000, startsWith: 5000, includes: 2000 },\n subtitle: { exact: 2000, startsWith: 1000, includes: 500 },\n body: { exact: 1000, startsWith: 600, includes: 400 },\n tag: { exact: 500, startsWith: 400, includes: 300 },\n meta: { exact: 200, startsWith: 150, includes: 100 },\n id: { exact: 100, startsWith: 75, includes: 50 },\n};\n\n/**\n * Below this query length we do NOT auto-match the row `id`. A 1–2 char query\n * is almost always a substring of *some* hex chars in *every* UUID, so matching\n * id at that length would flood results with the whole table. From 3 chars up a\n * partial-UUID paste is selective enough to be a real lookup.\n */\nconst MIN_AUTO_ID_QUERY_LEN = 3;\n\n/** Pull a non-empty string `id` off an item, or null if it has none. */\nfunction getStringId(item: unknown): string | null {\n if (item && typeof item === \"object\" && \"id\" in item) {\n const id = (item as { id?: unknown }).id;\n if (typeof id === \"string\" && id.length > 0) return id;\n }\n return null;\n}\n\nfunction resolveTiers(field: SearchFieldConfig<unknown>) {\n if (field.weight === \"custom\" || field.exact != null) {\n return {\n exact: field.exact ?? 0,\n startsWith: field.startsWith ?? 0,\n includes: field.includes ?? 0,\n };\n }\n return WEIGHT_TABLE[field.weight ?? \"body\"];\n}\n\nfunction scoreValue(\n value: string,\n q: string,\n tiers: { exact: number; startsWith: number; includes: number },\n): number {\n if (!value) return 0;\n const v = value.toLowerCase();\n if (v === q) return tiers.exact;\n if (v.startsWith(q)) return tiers.startsWith;\n if (v.includes(q)) return tiers.includes;\n return 0;\n}\n\n/**\n * Compute a weighted relevance score for `item` against `query`.\n * Returns 0 if there is no match — callers can treat `> 0` as a match predicate.\n *\n * Within each field, multiple values (e.g. tags) contribute the BEST match,\n * not the sum, so an item with many tags doesn't unfairly outrank one with a\n * single exact title match.\n */\nexport function computeSearchScore<T>(\n item: T,\n query: string,\n fields: SearchFieldConfig<T>[],\n): number {\n const trimmed = query.trim();\n if (!trimmed) return 0;\n const q = trimmed.toLowerCase();\n\n let total = 0;\n fields.forEach((field, idx) => {\n const raw = field.get(item);\n if (raw == null) return;\n const tiers = resolveTiers(field as SearchFieldConfig<unknown>);\n\n let best = 0;\n if (Array.isArray(raw)) {\n for (const v of raw) {\n if (typeof v !== \"string\") continue;\n const s = scoreValue(v, q, tiers);\n if (s > best) best = s;\n }\n } else if (typeof raw === \"string\") {\n best = scoreValue(raw, q, tiers);\n }\n\n if (best > 0) {\n // Tiny bias so that when two fields tie, the one declared first wins.\n total += best + (fields.length - idx);\n }\n });\n\n // Auto-match the row's UUID `id` against EVERY search box. A user can paste a\n // full or partial id and find the record, without each callsite remembering\n // to declare an id field. Skipped when the caller already declared an\n // explicit `weight: \"id\"` field (so we don't double-score), and gated on a\n // minimum query length so short queries don't match random hex substrings.\n const hasExplicitId = fields.some((f) => f.weight === \"id\");\n if (!hasExplicitId && q.length >= MIN_AUTO_ID_QUERY_LEN) {\n const id = getStringId(item);\n if (id) {\n total += scoreValue(id, q, WEIGHT_TABLE.id);\n }\n }\n\n return total;\n}\n\nexport function matchesSearch<T>(\n item: T,\n query: string,\n fields: SearchFieldConfig<T>[],\n): boolean {\n return computeSearchScore(item, query, fields) > 0;\n}\n\n/**\n * Drop-in id-match for hand-rolled `.filter()` predicates that can't (yet) move\n * onto {@link filterAndSortBySearch}. Returns true when `query` is a substring\n * of the item's string `id`, applying the same {@link MIN_AUTO_ID_QUERY_LEN}\n * guard as the automatic pass so short queries don't match random hex.\n *\n * list.filter((x) => x.name.toLowerCase().includes(q) || idMatchesQuery(x, q))\n *\n * Prefer migrating the callsite to `filterAndSortBySearch` (which does this for\n * free); reach for this only when an existing custom sort must be preserved.\n */\nexport function idMatchesQuery(item: unknown, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (q.length < MIN_AUTO_ID_QUERY_LEN) return false;\n const id = getStringId(item);\n return id != null && id.toLowerCase().includes(q);\n}\n\n/**\n * Filter out non-matches and sort remaining items by descending relevance.\n * Stable with respect to the original order when two items tie.\n */\nexport function filterAndSortBySearch<T>(\n items: readonly T[],\n query: string,\n fields: SearchFieldConfig<T>[],\n): T[] {\n const trimmed = query.trim();\n if (!trimmed) return items.slice();\n\n const scored: { item: T; score: number; idx: number }[] = [];\n for (let i = 0; i < items.length; i++) {\n const item = items[i] as T;\n const score = computeSearchScore(item, trimmed, fields);\n if (score > 0) scored.push({ item, score, idx: i });\n }\n scored.sort((a, b) => (b.score - a.score) || (a.idx - b.idx));\n return scored.map((s) => s.item);\n}\n"],"mappings":"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","names":[]}