@cavuno/board 3.1.0 → 4.0.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 (77) hide show
  1. package/CHANGELOG.md +421 -0
  2. package/README.md +6 -2
  3. package/dist/{_spec-nkWAraS0.d.mts → _spec-GVe6WAfT.d.mts} +68 -59
  4. package/dist/{_spec-nkWAraS0.d.ts → _spec-GVe6WAfT.d.ts} +68 -59
  5. package/dist/bin.mjs +7 -2
  6. package/dist/blog-BsmKizpd.d.ts +29 -0
  7. package/dist/blog-Cf64mI8A.d.mts +29 -0
  8. package/dist/{salaries-BahBpxWT.d.ts → board-DUhgiacm.d.ts} +17 -29
  9. package/dist/{salaries-ChSqDBAM.d.mts → board-GGWJ4coe.d.mts} +17 -29
  10. package/dist/breadcrumbs-DFKr0HzU.d.mts +372 -0
  11. package/dist/breadcrumbs-DiccJhk4.d.ts +372 -0
  12. package/dist/chunk-2ETP5UH4.mjs +271 -0
  13. package/dist/chunk-BGKNXG4H.mjs +146 -0
  14. package/dist/chunk-DLSO6XXY.mjs +33 -0
  15. package/dist/chunk-T2KC6XGV.mjs +1060 -0
  16. package/dist/chunk-YEAQ3JEI.mjs +853 -0
  17. package/dist/doctor.js +1 -1
  18. package/dist/doctor.mjs +1 -1
  19. package/dist/filters.d.mts +25 -20
  20. package/dist/filters.d.ts +25 -20
  21. package/dist/filters.js +3 -692
  22. package/dist/filters.mjs +3 -690
  23. package/dist/format.d.mts +57 -230
  24. package/dist/format.d.ts +57 -230
  25. package/dist/format.js +355 -927
  26. package/dist/format.mjs +213 -931
  27. package/dist/go.js +11 -1
  28. package/dist/go.mjs +4 -29
  29. package/dist/index.d.mts +23 -75
  30. package/dist/index.d.ts +23 -75
  31. package/dist/index.js +11 -36
  32. package/dist/index.mjs +32 -313
  33. package/dist/{jobs-lVvKCBCU.d.mts → jobs-BuLpV8iy.d.mts} +4 -4
  34. package/dist/{jobs-BiEvL9d7.d.ts → jobs-QrbXOYB9.d.ts} +4 -4
  35. package/dist/paths.d.mts +89 -1
  36. package/dist/paths.d.ts +89 -1
  37. package/dist/paths.js +62 -14
  38. package/dist/paths.mjs +29 -78
  39. package/dist/route-contract.js +23 -13
  40. package/dist/route-contract.mjs +30 -1079
  41. package/dist/{search-CCyFWPi3.d.mts → search-Cr4noJJZ.d.mts} +14 -3
  42. package/dist/{search-CyRvVMUp.d.ts → search-DdsniXQv.d.ts} +14 -3
  43. package/dist/seo.d.mts +33 -189
  44. package/dist/seo.d.ts +33 -189
  45. package/dist/seo.js +197 -718
  46. package/dist/seo.mjs +44 -1417
  47. package/dist/server.d.mts +5 -5
  48. package/dist/server.d.ts +5 -5
  49. package/dist/server.mjs +5 -31
  50. package/dist/sitemap.d.mts +5 -5
  51. package/dist/sitemap.d.ts +5 -5
  52. package/dist/sitemap.js +25 -14
  53. package/dist/sitemap.mjs +22 -97
  54. package/dist/skills.js +1 -1
  55. package/dist/skills.mjs +1 -1
  56. package/dist/suggest.d.mts +2 -2
  57. package/dist/suggest.d.ts +2 -2
  58. package/dist/suggest.js +17 -3
  59. package/dist/suggest.mjs +17 -3
  60. package/dist/well-known.js +23 -13
  61. package/dist/well-known.mjs +4 -421
  62. package/package.json +5 -4
  63. package/skills/cavuno-board-applications/SKILL.md +4 -1
  64. package/skills/cavuno-board-filters/SKILL.md +11 -12
  65. package/skills/cavuno-board-format/SKILL.md +68 -56
  66. package/skills/cavuno-board-i18n/SKILL.md +102 -21
  67. package/skills/cavuno-board-search-suggestions/SKILL.md +8 -3
  68. package/skills/cavuno-board-seo/BLOG.md +4 -1
  69. package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +5 -2
  70. package/skills/cavuno-board-seo/LISTING.md +35 -12
  71. package/skills/cavuno-board-seo/SALARY.md +83 -18
  72. package/skills/cavuno-board-seo/SKILL.md +3 -2
  73. package/skills/manifest.json +3 -3
  74. package/dist/board-NE98PlwN.d.ts +0 -16
  75. package/dist/board-muu8BI8P.d.mts +0 -16
  76. package/dist/ui-copy-CCkz6CVF.d.mts +0 -230
  77. package/dist/ui-copy-CCkz6CVF.d.ts +0 -230
@@ -1,424 +1,7 @@
1
- // src/paths/index.ts
2
- function jobDetailPath(companySlug, jobSlug) {
3
- return `/companies/${companySlug}/jobs/${jobSlug}`;
4
- }
5
- function jobsCategoryPath(categorySlug) {
6
- return `/jobs/${categorySlug}`;
7
- }
8
- function jobsSkillPath(skillSlug) {
9
- return `/jobs/skills/${skillSlug}`;
10
- }
11
- function jobsLocationPath(placeSlug) {
12
- return `/jobs/locations/${placeSlug}`;
13
- }
14
- function companyPath(companySlug) {
15
- return `/companies/${companySlug}`;
16
- }
17
- function companyMarketPath(marketSlug) {
18
- return `/companies/markets/${marketSlug}`;
19
- }
20
- function companySalaryPath(companySlug) {
21
- return `/companies/${companySlug}/salaries`;
22
- }
23
- function salaryTitlePath(titleSlug) {
24
- return `/salaries/titles/${titleSlug}`;
25
- }
26
- function salarySkillPath(skillSlug) {
27
- return `/salaries/skills/${skillSlug}`;
28
- }
29
- function salaryLocationPath(placeSlug) {
30
- return `/salaries/locations/${placeSlug}`;
31
- }
32
- function blogPostPath(postSlug) {
33
- return `/blog/${postSlug}`;
34
- }
35
- function blogTagPath(tagSlug) {
36
- return `/blog/tag/${tagSlug}`;
37
- }
38
- function blogAuthorPath(authorSlug) {
39
- return `/blog/author/${authorSlug}`;
40
- }
41
- var BOARD_PATHS = {
42
- home: "/",
43
- jobs: "/jobs",
44
- companies: "/companies",
45
- salaries: "/salaries",
46
- salaryCompanies: "/salaries/companies",
47
- salaryTitles: "/salaries/titles",
48
- salarySkills: "/salaries/skills",
49
- salaryLocations: "/salaries/locations",
50
- blog: "/blog",
51
- about: "/about",
52
- privacyPolicy: "/privacy-policy",
53
- termsOfService: "/terms-of-service",
54
- cookiePolicy: "/cookie-policy",
55
- impressum: "/impressum",
56
- talent: "/talent",
57
- employers: "/employers",
58
- /**
59
- * Alert-email surfaces. Promoted out of raw literals in
60
- * the job-alerts composer so /go indirection and email composition share
61
- * one definition.
62
- */
63
- alertsManage: "/alerts/manage",
64
- alertsConfirm: "/alerts/confirm"
65
- };
66
-
67
- // src/route-contract/roles.ts
68
- var ROLE_PARAM_REGISTRY = {
69
- jobDetail: ["companySlug", "jobSlug"],
70
- jobsCategory: ["categorySlug"],
71
- jobsSkill: ["skillSlug"],
72
- jobsLocation: ["placeSlug"],
73
- company: ["companySlug"],
74
- companyMarket: ["marketSlug"],
75
- companySalary: ["companySlug"],
76
- salaryTitle: ["titleSlug"],
77
- salarySkill: ["skillSlug"],
78
- salaryLocation: ["placeSlug"],
79
- blogPost: ["postSlug"],
80
- blogTag: ["tagSlug"],
81
- blogAuthor: ["authorSlug"],
82
- home: [],
83
- jobs: [],
84
- companies: [],
85
- salaries: [],
86
- salaryCompanies: [],
87
- salaryTitles: [],
88
- salarySkills: [],
89
- salaryLocations: [],
90
- blog: [],
91
- about: [],
92
- privacyPolicy: [],
93
- termsOfService: [],
94
- cookiePolicy: [],
95
- impressum: [],
96
- talent: [],
97
- employers: [],
98
- alertsManage: [],
99
- alertsConfirm: []
100
- };
101
- var REQUIRED_ROLES = [
102
- "jobDetail",
103
- "alertsManage",
104
- "alertsConfirm"
105
- ];
106
- var ALL_ROLES = Object.keys(
107
- ROLE_PARAM_REGISTRY
108
- );
109
- var ROLE_SET = new Set(ALL_ROLES);
110
- var CANONICAL_MANIFEST = {
111
- version: 1,
112
- roles: {
113
- jobDetail: jobDetailPath(":companySlug", ":jobSlug"),
114
- jobsCategory: jobsCategoryPath(":categorySlug"),
115
- jobsSkill: jobsSkillPath(":skillSlug"),
116
- jobsLocation: jobsLocationPath(":placeSlug"),
117
- company: companyPath(":companySlug"),
118
- companyMarket: companyMarketPath(":marketSlug"),
119
- companySalary: companySalaryPath(":companySlug"),
120
- salaryTitle: salaryTitlePath(":titleSlug"),
121
- salarySkill: salarySkillPath(":skillSlug"),
122
- salaryLocation: salaryLocationPath(":placeSlug"),
123
- blogPost: blogPostPath(":postSlug"),
124
- blogTag: blogTagPath(":tagSlug"),
125
- blogAuthor: blogAuthorPath(":authorSlug"),
126
- home: BOARD_PATHS.home,
127
- jobs: BOARD_PATHS.jobs,
128
- companies: BOARD_PATHS.companies,
129
- salaries: BOARD_PATHS.salaries,
130
- salaryCompanies: BOARD_PATHS.salaryCompanies,
131
- salaryTitles: BOARD_PATHS.salaryTitles,
132
- salarySkills: BOARD_PATHS.salarySkills,
133
- salaryLocations: BOARD_PATHS.salaryLocations,
134
- blog: BOARD_PATHS.blog,
135
- about: BOARD_PATHS.about,
136
- privacyPolicy: BOARD_PATHS.privacyPolicy,
137
- termsOfService: BOARD_PATHS.termsOfService,
138
- cookiePolicy: BOARD_PATHS.cookiePolicy,
139
- impressum: BOARD_PATHS.impressum,
140
- talent: BOARD_PATHS.talent,
141
- employers: BOARD_PATHS.employers,
142
- alertsManage: "/alerts/manage",
143
- alertsConfirm: "/alerts/confirm"
144
- }
145
- };
146
-
147
- // src/route-contract/params.ts
148
- function extractParamNames(template) {
149
- const names = [];
150
- const re = /:([A-Za-z_][A-Za-z0-9_]*)/g;
151
- let match;
152
- while ((match = re.exec(template)) !== null) {
153
- const name = match[1];
154
- if (name !== void 0) names.push(name);
155
- }
156
- return names;
157
- }
158
- function sameParamSet(a, b) {
159
- if (a.length !== b.length) return false;
160
- const sortedA = [...a].sort();
161
- const sortedB = [...b].sort();
162
- for (let i = 0; i < sortedA.length; i++) {
163
- if (sortedA[i] !== sortedB[i]) return false;
164
- }
165
- return true;
166
- }
167
- function paramSetKey(params) {
168
- return [...params].sort().join("\0");
169
- }
170
-
171
- // src/route-contract/classify.ts
172
- var SHARED_SIGNATURE_COUNTS = (() => {
173
- const counts = /* @__PURE__ */ new Map();
174
- for (const role of ALL_ROLES) {
175
- const params = ROLE_PARAM_REGISTRY[role];
176
- if (params.length === 0) continue;
177
- const key = paramSetKey(params);
178
- counts.set(key, (counts.get(key) ?? 0) + 1);
179
- }
180
- return counts;
181
- })();
182
- function isSignatureCandidate(route, role) {
183
- const registry = ROLE_PARAM_REGISTRY[role];
184
- const routeParams = extractParamNames(route.template);
185
- if (registry.length === 0) {
186
- return route.template === CANONICAL_MANIFEST.roles[role];
187
- }
188
- if (!sameParamSet(routeParams, registry)) return false;
189
- const shareCount = SHARED_SIGNATURE_COUNTS.get(paramSetKey(registry)) ?? 0;
190
- if (shareCount > 1) {
191
- return route.template === CANONICAL_MANIFEST.roles[role];
192
- }
193
- return true;
194
- }
195
- function classifyRoutes(routes, markers) {
196
- const assignments = {};
197
- const ambiguities = [];
198
- const claimed = /* @__PURE__ */ new Set();
199
- if (markers && markers.size > 0) {
200
- const claimsByRole = /* @__PURE__ */ new Map();
201
- for (const route of routes) {
202
- if (route.sourcePath === void 0) continue;
203
- const role = markers.get(route.sourcePath);
204
- if (role === void 0) continue;
205
- const list = claimsByRole.get(role);
206
- if (list) list.push(route);
207
- else claimsByRole.set(role, [route]);
208
- }
209
- for (const [role, candidates] of claimsByRole) {
210
- for (const c of candidates) claimed.add(c);
211
- if (candidates.length === 1) {
212
- assignments[role] = candidates[0];
213
- } else {
214
- ambiguities.push({ role, candidates: [...candidates] });
215
- }
216
- }
217
- }
218
- const pool = routes.filter((r) => !claimed.has(r));
219
- for (const role of ALL_ROLES) {
220
- if (assignments[role] !== void 0) continue;
221
- if (ambiguities.some((a) => a.role === role)) continue;
222
- const candidates = pool.filter((r) => isSignatureCandidate(r, role));
223
- if (candidates.length === 0) continue;
224
- if (candidates.length === 1) {
225
- const only = candidates[0];
226
- if (claimed.has(only)) {
227
- continue;
228
- }
229
- assignments[role] = only;
230
- claimed.add(only);
231
- continue;
232
- }
233
- ambiguities.push({ role, candidates: [...candidates] });
234
- }
235
- const missingRequired = REQUIRED_ROLES.filter(
236
- (role) => assignments[role] === void 0
237
- );
238
- return { assignments, ambiguities, missingRequired };
239
- }
240
-
241
- // src/route-contract/validate.ts
242
- var TEMPLATE_CHARSET_RE = /^[A-Za-z0-9/_.:-]+$/;
243
- function checkRoleTemplate(role, template) {
244
- const errors = [];
245
- if (typeof template !== "string" || template.length === 0) {
246
- errors.push({
247
- code: "invalid_template",
248
- message: `role '${role}' template must be a non-empty string`,
249
- role
250
- });
251
- return errors;
252
- }
253
- if (!template.startsWith("/")) {
254
- errors.push({
255
- code: "invalid_template",
256
- message: `role '${role}' template must start with '/', got ${JSON.stringify(template)}`,
257
- role
258
- });
259
- return errors;
260
- }
261
- if (/\s/.test(template)) {
262
- errors.push({
263
- code: "invalid_template",
264
- message: `role '${role}' template must not contain whitespace, got ${JSON.stringify(template)}`,
265
- role
266
- });
267
- return errors;
268
- }
269
- if (template.includes("?") || template.includes("#")) {
270
- errors.push({
271
- code: "template_query_or_fragment",
272
- message: `role '${role}' template must not contain '?' or '#', got ${JSON.stringify(template)}`,
273
- role
274
- });
275
- return errors;
276
- }
277
- if (template.startsWith("//") || template.includes("//")) {
278
- errors.push({
279
- code: "template_double_slash",
280
- message: `role '${role}' template must not contain '//', got ${JSON.stringify(template)}`,
281
- role
282
- });
283
- return errors;
284
- }
285
- for (const seg of template.split("/")) {
286
- if (seg === "." || seg === "..") {
287
- errors.push({
288
- code: "template_dot_segment",
289
- message: `role '${role}' template must not contain '.' or '..' segments, got ${JSON.stringify(template)}`,
290
- role
291
- });
292
- return errors;
293
- }
294
- }
295
- if (!TEMPLATE_CHARSET_RE.test(template)) {
296
- errors.push({
297
- code: "template_illegal_charset",
298
- message: `role '${role}' template contains illegal characters, got ${JSON.stringify(template)}`,
299
- role
300
- });
301
- return errors;
302
- }
303
- const registry = ROLE_PARAM_REGISTRY[role];
304
- const tokens = extractParamNames(template);
305
- if (registry.length === 0) {
306
- if (tokens.length > 0) {
307
- errors.push({
308
- code: "static_has_params",
309
- message: `static role '${role}' template must not contain ':' tokens, got ${template}`,
310
- role,
311
- token: tokens[0]
312
- });
313
- }
314
- } else {
315
- const registrySet = new Set(registry);
316
- for (const token of tokens) {
317
- if (!registrySet.has(token)) {
318
- errors.push({
319
- code: "unregistered_param",
320
- message: `role '${role}' template references unregistered param ':${token}'`,
321
- role,
322
- token
323
- });
324
- }
325
- }
326
- const retained = tokens.filter((t) => registrySet.has(t));
327
- if (retained.length === 0) {
328
- errors.push({
329
- code: "missing_entity_param",
330
- message: `role '${role}' template must contain at least one registry param (${registry.join(", ")})`,
331
- role
332
- });
333
- }
334
- }
335
- return errors;
336
- }
337
-
338
- // src/route-contract/compile.ts
339
- var REQUIRED_SET = new Set(REQUIRED_ROLES);
340
- function compileManifest(routes, markers) {
341
- const { assignments, ambiguities, missingRequired } = classifyRoutes(
342
- routes,
343
- markers
344
- );
345
- const roles = {};
346
- const warnings = [];
347
- const blocking = new Set(missingRequired);
348
- for (const role of ALL_ROLES) {
349
- const entry = assignments[role];
350
- if (entry === void 0) continue;
351
- const templateErrors = checkRoleTemplate(role, entry.template);
352
- if (templateErrors.length > 0) {
353
- const exact = templateErrors.map((e) => e.message).join("; ");
354
- warnings.push(
355
- `role '${role}' template rejected: ${exact} \u2014 add export const cavunoPage = '${role}' on the correct page or fix the template`
356
- );
357
- if (REQUIRED_SET.has(role)) {
358
- blocking.add(role);
359
- }
360
- continue;
361
- }
362
- roles[role] = entry.template;
363
- }
364
- for (const amb of ambiguities) {
365
- const kind = REQUIRED_SET.has(amb.role) ? "required" : "optional";
366
- warnings.push(
367
- `${kind} role '${amb.role}' is ambiguous \u2014 add export const cavunoPage = '${amb.role}' to disambiguate`
368
- );
369
- }
370
- for (const role of ALL_ROLES) {
371
- if (REQUIRED_SET.has(role)) continue;
372
- if (roles[role] !== void 0) continue;
373
- if (assignments[role] !== void 0) continue;
374
- if (ambiguities.some((a) => a.role === role)) continue;
375
- warnings.push(`optional role '${role}' is unassigned`);
376
- }
377
- return {
378
- manifest: { version: 1, roles },
379
- blockingMissing: REQUIRED_ROLES.filter((r) => blocking.has(r)),
380
- warnings,
381
- ambiguities
382
- };
383
- }
384
-
385
- // src/route-contract/resolve-links.ts
386
- var REQUIRED_SET2 = new Set(REQUIRED_ROLES);
387
-
388
- // src/route-contract/match-history.ts
389
- var ROLE_INDEX_PATHS = {
390
- jobDetail: BOARD_PATHS.jobs,
391
- jobsCategory: BOARD_PATHS.jobs,
392
- jobsSkill: BOARD_PATHS.jobs,
393
- jobsLocation: BOARD_PATHS.jobs,
394
- company: BOARD_PATHS.companies,
395
- companyMarket: BOARD_PATHS.companies,
396
- companySalary: BOARD_PATHS.companies,
397
- salaryTitle: BOARD_PATHS.salaries,
398
- salarySkill: BOARD_PATHS.salaries,
399
- salaryLocation: BOARD_PATHS.salaries,
400
- salaryCompanies: BOARD_PATHS.salaries,
401
- salaryTitles: BOARD_PATHS.salaries,
402
- salarySkills: BOARD_PATHS.salaries,
403
- salaryLocations: BOARD_PATHS.salaries,
404
- salaries: BOARD_PATHS.salaries,
405
- blogPost: BOARD_PATHS.blog,
406
- blogTag: BOARD_PATHS.blog,
407
- blogAuthor: BOARD_PATHS.blog,
408
- blog: BOARD_PATHS.blog,
409
- home: BOARD_PATHS.home,
410
- jobs: BOARD_PATHS.jobs,
411
- companies: BOARD_PATHS.companies,
412
- about: BOARD_PATHS.about,
413
- privacyPolicy: BOARD_PATHS.privacyPolicy,
414
- termsOfService: BOARD_PATHS.termsOfService,
415
- cookiePolicy: BOARD_PATHS.cookiePolicy,
416
- impressum: BOARD_PATHS.impressum,
417
- talent: BOARD_PATHS.talent,
418
- employers: BOARD_PATHS.employers,
419
- alertsManage: BOARD_PATHS.home,
420
- alertsConfirm: BOARD_PATHS.home
421
- };
1
+ import {
2
+ compileManifest
3
+ } from "./chunk-T2KC6XGV.mjs";
4
+ import "./chunk-BGKNXG4H.mjs";
422
5
 
423
6
  // src/well-known/handler.ts
424
7
  var WELL_KNOWN_CACHE_CONTROL = "public, max-age=300";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavuno/board",
3
- "version": "3.1.0",
3
+ "version": "4.0.0",
4
4
  "description": "Build custom job boards and careers pages with Cavuno’s TypeScript SDK.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -201,9 +201,9 @@
201
201
  "scripts": {
202
202
  "build": "rm -rf dist && tsup",
203
203
  "clean": "git clean -xdf .turbo dist node_modules src/generated",
204
- "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.node.json",
204
+ "typecheck": "tsgo --noEmit && tsgo --noEmit -p tsconfig.node.json",
205
205
  "test": "vitest run",
206
- "check:publish": "publint --strict && attw --pack . --profile node16",
206
+ "check:publish": "publint --strict && attw --pack . --profile node16 && node scripts/check-no-private-kit-types.mjs",
207
207
  "gen:skills-manifest": "tsx scripts/generate-skills-manifest.ts",
208
208
  "assert-publish-target": "node -e \"const p=require('./package.json'); if(p.name!=='@cavuno/board'){throw new Error('Refusing to publish: package.json name is '+p.name+', expected @cavuno/board')}; if(p.private){throw new Error('Refusing to publish: package.json has private:true')}\"",
209
209
  "prepublishOnly": "pnpm run assert-publish-target && pnpm run gen:skills-manifest && pnpm run build"
@@ -215,7 +215,8 @@
215
215
  "tsup": "^8.4.0",
216
216
  "tsx": "^4.19.2",
217
217
  "vitest": "^2.1.9",
218
- "typescript": "^5.9.3"
218
+ "typescript": "^5.9.3",
219
+ "@typescript/native-preview": "7.0.0-dev.20260707.2"
219
220
  },
220
221
  "engines": {
221
222
  "node": ">=20"
@@ -76,7 +76,10 @@ await board.me.applications.withdraw(applicationId);
76
76
 
77
77
  ## Save jobs
78
78
 
79
- Saved rows embed the full `PublicJob`. `save` converges on the existing row and `unsave` is idempotent.
79
+ Saved rows embed the same slim `PublicJobCard` as the jobs list — render them
80
+ with the same card view-model, and do not expect full-job fields (description,
81
+ custom field values) on saved rows. `save` converges on the existing row and
82
+ `unsave` is idempotent.
80
83
 
81
84
  ```ts snippet
82
85
  const saved = await board.me.savedJobs.list({ limit: 20 });
@@ -49,21 +49,19 @@ import {
49
49
  JOB_SORTS,
50
50
  REMOTE_OPTIONS,
51
51
  SENIORITIES,
52
- seniorityLabels,
53
- sortLabels,
54
52
  } from '@cavuno/board/filters';
55
53
 
56
- const { language } = await board.context();
57
- const seniorityCopy = seniorityLabels(language);
58
- const sortCopy = sortLabels(language);
54
+ // Wire enums only — display labels are application-owned chrome.
55
+ const seniorityOptions = SENIORITIES;
56
+ const sortOptions = JOB_SORTS;
59
57
  ```
60
58
 
61
59
  Render seniority as a multi-select with all eight `SENIORITIES`.
62
60
  `EMPLOYMENT_TYPES` contains five listing options; `volunteer` and `other`
63
61
  remain valid job wire values but are absent from the filter control.
64
62
  `JOB_SORTS` contains exactly `relevance`, `newest`, and `salary_high`;
65
- `relevance` is the featured-ranked default. Sort labels follow the board copy
66
- catalog and operator overrides supplied to `sortLabels`.
63
+ `relevance` is the featured-ranked default. Label each option with your
64
+ application's copy (message catalog or hard-coded board language).
67
65
 
68
66
  ## Load taxonomy options
69
67
 
@@ -95,15 +93,16 @@ const second = first.nextCursor
95
93
  : null;
96
94
  ```
97
95
 
98
- Keyword suggestions accept `q` and `limit`. A present query shorter than two
99
- characters returns no results. Suggestions contain categories and skills only;
100
- because both types may share a slug, key each option by
101
- `type + canonicalSlug`.
96
+ Keyword suggestions accept `q`, `limit`, and optional `types`. A present query
97
+ shorter than two characters returns no results. Restrict to taxonomy terms with
98
+ `types: ['category', 'skill']`; because both may share a slug, key each option
99
+ by `termType + canonicalSlug`.
102
100
 
103
101
  ```ts snippet
104
- const suggestions = await board.taxonomy.suggestions.list({
102
+ const { items } = await board.search.suggest({
105
103
  q: searchText,
106
104
  limit: 10,
105
+ types: ['category', 'skill'],
107
106
  });
108
107
  ```
109
108