@cavuno/board 1.37.0 → 1.39.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/README.md +7 -0
- package/dist/{jobs-CMCADU_-.d.mts → _spec-DxC1ze93.d.mts} +57 -100
- package/dist/{jobs-CMCADU_-.d.ts → _spec-DxC1ze93.d.ts} +57 -100
- package/dist/{board-D_BRa2zC.d.ts → board-CqYibYUA.d.mts} +1 -1
- package/dist/{board-D0guOBYy.d.mts → board-RfZEAJse.d.ts} +1 -1
- package/dist/filters.d.mts +18 -4
- package/dist/filters.d.ts +18 -4
- package/dist/filters.js +17 -0
- package/dist/filters.mjs +17 -0
- package/dist/format.d.mts +3 -2
- package/dist/format.d.ts +3 -2
- package/dist/index.d.mts +17 -73
- package/dist/index.d.ts +17 -73
- package/dist/index.js +22 -1
- package/dist/index.mjs +22 -1
- package/dist/jobs-CLLIvtMc.d.ts +105 -0
- package/dist/jobs-DPPA1Nev.d.mts +105 -0
- package/dist/{salaries-9U42CM5A.d.mts → salaries-DK4RnJnw.d.ts} +2 -1
- package/dist/{salaries-C3w9kvPJ.d.ts → salaries-Rb5h_eVZ.d.mts} +2 -1
- package/dist/search-CqBa1Qc4.d.mts +81 -0
- package/dist/search-DBoMM-gE.d.ts +81 -0
- package/dist/seo.d.mts +4 -3
- package/dist/seo.d.ts +4 -3
- package/dist/server.d.mts +5 -3
- package/dist/server.d.ts +5 -3
- package/dist/sitemap.d.mts +5 -3
- package/dist/sitemap.d.ts +5 -3
- package/dist/suggest.d.mts +70 -0
- package/dist/suggest.d.ts +70 -0
- package/dist/suggest.js +165 -0
- package/dist/suggest.mjs +144 -0
- package/package.json +11 -1
- package/skills/cavuno-board-filters/SKILL.md +11 -4
- package/skills/cavuno-board-jobs/SKILL.md +18 -2
- package/skills/cavuno-board-suggest/SKILL.md +119 -0
- package/skills/manifest.json +9 -2
package/dist/suggest.mjs
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// src/suggest/index.ts
|
|
2
|
+
var DEFAULT_MIN_CHARS = 2;
|
|
3
|
+
var DEFAULT_DEBOUNCE_MS = 250;
|
|
4
|
+
function isAbortError(error) {
|
|
5
|
+
return typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
|
|
6
|
+
}
|
|
7
|
+
function applyExclusions(items, excluded) {
|
|
8
|
+
if (excluded.size === 0) return items.slice();
|
|
9
|
+
return items.filter(
|
|
10
|
+
(item) => !(item.type === "company" && excluded.has(item.slug.toLowerCase()))
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
function createSuggestController(board, options) {
|
|
14
|
+
const minChars = options?.minChars ?? DEFAULT_MIN_CHARS;
|
|
15
|
+
const debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
16
|
+
const limit = options?.limit;
|
|
17
|
+
let disposed = false;
|
|
18
|
+
let state = {
|
|
19
|
+
query: "",
|
|
20
|
+
items: [],
|
|
21
|
+
status: "idle",
|
|
22
|
+
error: null
|
|
23
|
+
};
|
|
24
|
+
let rawItems = [];
|
|
25
|
+
let excluded = /* @__PURE__ */ new Set();
|
|
26
|
+
let sequence = 0;
|
|
27
|
+
let timer = null;
|
|
28
|
+
let abortController = null;
|
|
29
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
30
|
+
function notify() {
|
|
31
|
+
for (const listener of listeners) listener();
|
|
32
|
+
}
|
|
33
|
+
function setState(next) {
|
|
34
|
+
state = next;
|
|
35
|
+
notify();
|
|
36
|
+
}
|
|
37
|
+
function cancelTimer() {
|
|
38
|
+
if (timer !== null) {
|
|
39
|
+
globalThis.clearTimeout(timer);
|
|
40
|
+
timer = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function abortInFlight() {
|
|
44
|
+
if (abortController) {
|
|
45
|
+
abortController.abort();
|
|
46
|
+
abortController = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function runRequest(query) {
|
|
50
|
+
const seq = ++sequence;
|
|
51
|
+
abortInFlight();
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
abortController = controller;
|
|
54
|
+
const suggestQuery = { q: query };
|
|
55
|
+
if (limit !== void 0) suggestQuery.limit = limit;
|
|
56
|
+
void board.search.suggest(suggestQuery, { signal: controller.signal }).then((result) => {
|
|
57
|
+
if (disposed || seq !== sequence) return;
|
|
58
|
+
abortController = null;
|
|
59
|
+
rawItems = result.items;
|
|
60
|
+
setState({
|
|
61
|
+
query: state.query,
|
|
62
|
+
items: applyExclusions(rawItems, excluded),
|
|
63
|
+
status: "ready",
|
|
64
|
+
error: null
|
|
65
|
+
});
|
|
66
|
+
}).catch((error) => {
|
|
67
|
+
if (disposed || seq !== sequence) return;
|
|
68
|
+
if (isAbortError(error)) return;
|
|
69
|
+
abortController = null;
|
|
70
|
+
setState({
|
|
71
|
+
query: state.query,
|
|
72
|
+
items: state.items,
|
|
73
|
+
status: "error",
|
|
74
|
+
error
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
setQuery(query) {
|
|
80
|
+
if (disposed) return;
|
|
81
|
+
const trimmed = query.trim();
|
|
82
|
+
cancelTimer();
|
|
83
|
+
if (trimmed.length < minChars) {
|
|
84
|
+
sequence += 1;
|
|
85
|
+
abortInFlight();
|
|
86
|
+
rawItems = [];
|
|
87
|
+
setState({
|
|
88
|
+
query,
|
|
89
|
+
items: [],
|
|
90
|
+
status: "idle",
|
|
91
|
+
error: null
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
sequence += 1;
|
|
96
|
+
abortInFlight();
|
|
97
|
+
setState({
|
|
98
|
+
query,
|
|
99
|
+
items: state.items,
|
|
100
|
+
status: "loading",
|
|
101
|
+
error: null
|
|
102
|
+
});
|
|
103
|
+
timer = globalThis.setTimeout(() => {
|
|
104
|
+
timer = null;
|
|
105
|
+
if (disposed) return;
|
|
106
|
+
runRequest(trimmed);
|
|
107
|
+
}, debounceMs);
|
|
108
|
+
},
|
|
109
|
+
setExcludedCompanySlugs(slugs) {
|
|
110
|
+
if (disposed) return;
|
|
111
|
+
excluded = new Set(
|
|
112
|
+
slugs.map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
113
|
+
);
|
|
114
|
+
setState({
|
|
115
|
+
query: state.query,
|
|
116
|
+
items: applyExclusions(rawItems, excluded),
|
|
117
|
+
status: state.status,
|
|
118
|
+
error: state.error
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
getState() {
|
|
122
|
+
return state;
|
|
123
|
+
},
|
|
124
|
+
subscribe(listener) {
|
|
125
|
+
if (disposed) return () => {
|
|
126
|
+
};
|
|
127
|
+
listeners.add(listener);
|
|
128
|
+
return () => {
|
|
129
|
+
listeners.delete(listener);
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
dispose() {
|
|
133
|
+
if (disposed) return;
|
|
134
|
+
disposed = true;
|
|
135
|
+
cancelTimer();
|
|
136
|
+
abortInFlight();
|
|
137
|
+
sequence += 1;
|
|
138
|
+
listeners.clear();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export {
|
|
143
|
+
createSuggestController
|
|
144
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cavuno/board",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"description": "Typed isomorphic client for the Cavuno Board API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -50,6 +50,16 @@
|
|
|
50
50
|
"default": "./dist/filters.js"
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
|
+
"./suggest": {
|
|
54
|
+
"import": {
|
|
55
|
+
"types": "./dist/suggest.d.mts",
|
|
56
|
+
"default": "./dist/suggest.mjs"
|
|
57
|
+
},
|
|
58
|
+
"require": {
|
|
59
|
+
"types": "./dist/suggest.d.ts",
|
|
60
|
+
"default": "./dist/suggest.js"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
53
63
|
"./theme": {
|
|
54
64
|
"import": {
|
|
55
65
|
"types": "./dist/theme.d.mts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-filters
|
|
3
|
-
description: The canonical listing-filter vocabulary and URL search-param parsing with @cavuno/board/filters — remote/employment/seniority/sort sets, localized seniority labels, and parseListingFilters for jobs-listing routes. Use when building listing pages, filter sidebars, sort dropdowns, or validating listing URL params.
|
|
3
|
+
description: The canonical listing-filter vocabulary and URL search-param parsing with @cavuno/board/filters — remote/employment/seniority/company/sort sets, localized seniority labels, and parseListingFilters for jobs-listing routes. Use when building listing pages, filter sidebars, sort dropdowns, or validating listing URL params.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Filters: vocabulary + URL parsing
|
|
@@ -21,6 +21,7 @@ never throws).
|
|
|
21
21
|
- Turning values into card display text — `cavuno-board-format`.
|
|
22
22
|
- Search QUERIES (`jobs.search` bodies) — this module is about the listing
|
|
23
23
|
URL contract, not the search POST body.
|
|
24
|
+
- Search-dropdown typeahead — `cavuno-board-suggest`.
|
|
24
25
|
|
|
25
26
|
## Parse listing URLs
|
|
26
27
|
|
|
@@ -28,19 +29,25 @@ never throws).
|
|
|
28
29
|
import { parseListingFilters, DEFAULT_SORT } from '@cavuno/board/filters';
|
|
29
30
|
|
|
30
31
|
const filters = parseListingFilters(rawSearchParams);
|
|
31
|
-
// { q?, remoteOption?, employmentType?, seniority?: Seniority[], sort? }
|
|
32
|
+
// { q?, remoteOption?, employmentType?, seniority?: Seniority[], company?: string[], sort? }
|
|
32
33
|
|
|
33
34
|
const page = await board.jobs.list({
|
|
34
35
|
limit: 20,
|
|
35
36
|
seniority: filters.seniority,
|
|
37
|
+
companySlug: filters.company,
|
|
36
38
|
remoteOption: filters.remoteOption ? [filters.remoteOption] : undefined,
|
|
37
39
|
employmentType: filters.employmentType ? [filters.employmentType] : undefined,
|
|
38
40
|
});
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
Unknown values are dropped silently (public URLs, never throw). Seniority
|
|
42
|
-
|
|
43
|
-
rules: trim, lowercase, dedupe, keep order.
|
|
44
|
+
and `company` accept repeated params or a comma-string and normalize with
|
|
45
|
+
the hosted rules: trim, lowercase, dedupe, keep order. `company` is an open
|
|
46
|
+
value set (public slugs) capped at 10 (wire max; first 10 kept).
|
|
47
|
+
|
|
48
|
+
**Slugs are the URL identity.** Map `filters.company` to the wire as
|
|
49
|
+
`companySlug` (`jobs.list` query / `jobs.search` filters). The API accepts
|
|
50
|
+
slugs directly — never resolve slug→id client-side.
|
|
44
51
|
|
|
45
52
|
## Render the filter UI from the vocabulary
|
|
46
53
|
|
|
@@ -35,7 +35,7 @@ for (const card of page.data) {
|
|
|
35
35
|
|
|
36
36
|
### Filters and pagination
|
|
37
37
|
|
|
38
|
-
`JobsListQuery` supports `limit` (1–100), `offset` (takes precedence over `cursor`), `cursor`, and filters: `companyId`, `remoteOption`, `employmentType`, `seniority` (single or repeated → OR-matched), `location` + `radius` (km), `category`, `skill`. Paginate by passing back `nextCursor`, or use numbered pages with `offset`:
|
|
38
|
+
`JobsListQuery` supports `limit` (1–100), `offset` (takes precedence over `cursor`), `cursor`, and filters: `companyId`, `companySlug`, `remoteOption`, `employmentType`, `seniority` (single or repeated → OR-matched), `location` + `radius` (km), `category`, `skill`. Paginate by passing back `nextCursor`, or use numbered pages with `offset`:
|
|
39
39
|
|
|
40
40
|
```ts snippet
|
|
41
41
|
const p1 = await board.jobs.list({ limit: 20 });
|
|
@@ -46,9 +46,24 @@ const p2 = p1.nextCursor
|
|
|
46
46
|
const page3 = await board.jobs.list({ limit: 20, offset: 40 });
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
### Company filter via public slug
|
|
50
|
+
|
|
51
|
+
`companySlug` is the public URL identity (what listing URLs carry). Prefer it
|
|
52
|
+
over `companyId` in frontends — the API resolves slugs server-side. Unknown
|
|
53
|
+
slugs are **ignored** (they contribute no matches, not an error); an entirely
|
|
54
|
+
unknown set yields an empty result list with `count: 0`. Combined with
|
|
55
|
+
`companyId` as a union when both are set. Cap is 10 values.
|
|
56
|
+
|
|
57
|
+
```ts snippet
|
|
58
|
+
const page = await board.jobs.list({
|
|
59
|
+
limit: 20,
|
|
60
|
+
companySlug: ['acme', 'globex'],
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
49
64
|
## Search
|
|
50
65
|
|
|
51
|
-
`jobs.search` posts a `JobsSearchBody` (free-text `query` + structured `filters`) and returns a `JobCardSearchEnvelope`:
|
|
66
|
+
`jobs.search` posts a `JobsSearchBody` (free-text `query` + structured `filters`) and returns a `JobCardSearchEnvelope`. The same `companySlug` rule applies under `filters`:
|
|
52
67
|
|
|
53
68
|
```ts snippet
|
|
54
69
|
const results = await board.jobs.search({
|
|
@@ -56,6 +71,7 @@ const results = await board.jobs.search({
|
|
|
56
71
|
filters: {
|
|
57
72
|
seniority: ['senior'],
|
|
58
73
|
remoteOption: ['remote'],
|
|
74
|
+
companySlug: ['acme'],
|
|
59
75
|
publishedAt: { gte: '2026-01-01T00:00:00Z' },
|
|
60
76
|
},
|
|
61
77
|
limit: 20,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cavuno-board-suggest
|
|
3
|
+
description: Federated keyword search suggestions with the @cavuno/board SDK — board.search.suggest for companies + taxonomy terms in one server-ranked list, and the headless createSuggestController under @cavuno/board/suggest (debounce, abort, stale-drop). Use when building a search dropdown, typeahead, or autocomplete that mirrors the hosted board.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Search suggest: dropdown typeahead
|
|
7
|
+
|
|
8
|
+
`board.search.suggest` returns ONE interleaved, server-ordered list of
|
|
9
|
+
company and taxonomy-term suggestions for a keyword — the hosted search
|
|
10
|
+
dropdown contract. `@cavuno/board/suggest` wraps that call in a headless
|
|
11
|
+
controller so debounce / abort / stale-drop ship with the SDK and patch via
|
|
12
|
+
npm update.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- The global search input's suggestion dropdown.
|
|
17
|
+
- Any typeahead that should offer companies and category/skill terms
|
|
18
|
+
together, ranked the way the hosted board ranks them.
|
|
19
|
+
|
|
20
|
+
## When not to use
|
|
21
|
+
|
|
22
|
+
- Location autocomplete — `board.taxonomy.places.list({ q })`.
|
|
23
|
+
- Taxonomy-only option lists for filter sidebars —
|
|
24
|
+
`board.taxonomy.categories/skills/suggestions.list`.
|
|
25
|
+
- Job results themselves — `board.jobs.list` / `board.jobs.search`.
|
|
26
|
+
|
|
27
|
+
## Call the endpoint directly
|
|
28
|
+
|
|
29
|
+
```ts snippet
|
|
30
|
+
const { items, query } = await board.search.suggest({ q: 'acme', limit: 10 });
|
|
31
|
+
// items is server-ranked — do NOT re-sort.
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
if (item.type === 'company') {
|
|
34
|
+
item.slug; // public company URL slug
|
|
35
|
+
item.name;
|
|
36
|
+
item.jobCount;
|
|
37
|
+
} else {
|
|
38
|
+
// item.type === 'term'
|
|
39
|
+
item.termType; // 'category' | 'skill'
|
|
40
|
+
item.displayName;
|
|
41
|
+
item.canonicalSlug; // for links
|
|
42
|
+
item.sourceSlug; // for job filters
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Queries under two characters return an empty `items` array. `limit` is
|
|
48
|
+
1–25 (default 25). Order is the contract.
|
|
49
|
+
|
|
50
|
+
## Prefer the headless controller for UI
|
|
51
|
+
|
|
52
|
+
The controller owns debounce (default 250ms), min-chars (default 2),
|
|
53
|
+
in-flight abort, and stale-drop. Compatible with `useSyncExternalStore`.
|
|
54
|
+
|
|
55
|
+
```ts snippet
|
|
56
|
+
import { createSuggestController } from '@cavuno/board/suggest';
|
|
57
|
+
|
|
58
|
+
const suggest = createSuggestController(board, { limit: 10 });
|
|
59
|
+
const unsub = suggest.subscribe(() => {
|
|
60
|
+
const { items, status, query } = suggest.getState();
|
|
61
|
+
// render dropdown; previous items are retained while status === 'loading'
|
|
62
|
+
});
|
|
63
|
+
suggest.setQuery(inputValue);
|
|
64
|
+
// Hide the currently-applied company filter from the list:
|
|
65
|
+
suggest.setExcludedCompanySlugs(appliedCompanySlugs);
|
|
66
|
+
// teardown:
|
|
67
|
+
unsub();
|
|
68
|
+
suggest.dispose();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`setExcludedCompanySlugs` is the ONE permitted view-level filter (company
|
|
72
|
+
items only, re-filters current items synchronously — no new request). Never
|
|
73
|
+
reshape or re-sort the rest of the response.
|
|
74
|
+
|
|
75
|
+
## Wire a company pick into the listing
|
|
76
|
+
|
|
77
|
+
Company slugs are the URL identity. Map them onto the jobs surface as
|
|
78
|
+
`companySlug` — never resolve slug→id client-side:
|
|
79
|
+
|
|
80
|
+
```ts snippet
|
|
81
|
+
import { parseListingFilters } from '@cavuno/board/filters';
|
|
82
|
+
|
|
83
|
+
const filters = parseListingFilters(rawSearchParams);
|
|
84
|
+
// filters.company is string[] of public slugs
|
|
85
|
+
|
|
86
|
+
const page = await board.jobs.list({
|
|
87
|
+
limit: 20,
|
|
88
|
+
companySlug: filters.company,
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Unknown slugs are dropped server-side (not an error). If the entire company
|
|
93
|
+
filter resolves to no known companies, the listing is empty (`count: 0`).
|
|
94
|
+
Combined with `companyId` as a union when both are set.
|
|
95
|
+
|
|
96
|
+
## Anti-patterns
|
|
97
|
+
|
|
98
|
+
```ts no-check
|
|
99
|
+
// NEVER re-sort server-ranked items:
|
|
100
|
+
items.sort((a, b) => a.name.localeCompare(b.name));
|
|
101
|
+
// NEVER resolve company slug → id on the client:
|
|
102
|
+
const id = await lookupCompanyId(item.slug);
|
|
103
|
+
board.jobs.list({ companyId: [id] });
|
|
104
|
+
// NEVER hand-roll debounce/abort — use the controller so patches ship via npm.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Out of scope — do not invent exports
|
|
108
|
+
|
|
109
|
+
No React components, no dropdown UI, no keyboard navigation — this package
|
|
110
|
+
is the data + controller layer. Location suggest stays on `taxonomy.places`.
|
|
111
|
+
|
|
112
|
+
## Verify
|
|
113
|
+
|
|
114
|
+
- [ ] Typing under 2 characters shows no suggestions and no network call
|
|
115
|
+
from the controller.
|
|
116
|
+
- [ ] Fast typing cancels the previous request; only the final query's
|
|
117
|
+
results render.
|
|
118
|
+
- [ ] Selecting a company writes its public slug into the URL and the
|
|
119
|
+
listing request uses `companySlug`, not a client-resolved id.
|
package/skills/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.39.0",
|
|
3
3
|
"skills": [
|
|
4
4
|
{
|
|
5
5
|
"name": "cavuno-board-account",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
"name": "cavuno-board-filters",
|
|
55
|
-
"description": "The canonical listing-filter vocabulary and URL search-param parsing with @cavuno/board/filters — remote/employment/seniority/sort sets, localized seniority labels, and parseListingFilters for jobs-listing routes. Use when building listing pages, filter sidebars, sort dropdowns, or validating listing URL params.",
|
|
55
|
+
"description": "The canonical listing-filter vocabulary and URL search-param parsing with @cavuno/board/filters — remote/employment/seniority/company/sort sets, localized seniority labels, and parseListingFilters for jobs-listing routes. Use when building listing pages, filter sidebars, sort dropdowns, or validating listing URL params.",
|
|
56
56
|
"path": "skills/cavuno-board-filters/SKILL.md",
|
|
57
57
|
"framework": null,
|
|
58
58
|
"category": "core"
|
|
@@ -148,6 +148,13 @@
|
|
|
148
148
|
"framework": null,
|
|
149
149
|
"category": "core"
|
|
150
150
|
},
|
|
151
|
+
{
|
|
152
|
+
"name": "cavuno-board-suggest",
|
|
153
|
+
"description": "Federated keyword search suggestions with the @cavuno/board SDK — board.search.suggest for companies + taxonomy terms in one server-ranked list, and the headless createSuggestController under @cavuno/board/suggest (debounce, abort, stale-drop). Use when building a search dropdown, typeahead, or autocomplete that mirrors the hosted board.",
|
|
154
|
+
"path": "skills/cavuno-board-suggest/SKILL.md",
|
|
155
|
+
"framework": null,
|
|
156
|
+
"category": "core"
|
|
157
|
+
},
|
|
151
158
|
{
|
|
152
159
|
"name": "cavuno-board-tanstack-start",
|
|
153
160
|
"description": "TanStack-Start-on-Cloudflare-Workers reference wiring for a headless Cavuno board — SSR loaders calling @cavuno/board server-side, the session held in an __Host- httpOnly cookie owned by the app, a single-flight refresh helper, and FetchOptions cache passthrough on Workers.",
|