@cavuno/board 1.38.0 → 1.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 (45) hide show
  1. package/README.md +7 -0
  2. package/dist/{jobs-DMH3Ytoq.d.mts → _spec-DRbgPIlN.d.mts} +404 -96
  3. package/dist/{jobs-DMH3Ytoq.d.ts → _spec-DRbgPIlN.d.ts} +404 -96
  4. package/dist/bin.mjs +242 -18
  5. package/dist/{board-CBry4f7H.d.mts → board-BxPUtrOl.d.ts} +1 -1
  6. package/dist/{board-tV-zcaHs.d.ts → board-DCiNpgFf.d.mts} +1 -1
  7. package/dist/doctor.js +238 -14
  8. package/dist/doctor.mjs +238 -14
  9. package/dist/filters.d.mts +18 -4
  10. package/dist/filters.d.ts +18 -4
  11. package/dist/filters.js +17 -0
  12. package/dist/filters.mjs +17 -0
  13. package/dist/format.d.mts +3 -2
  14. package/dist/format.d.ts +3 -2
  15. package/dist/index.d.mts +62 -73
  16. package/dist/index.d.ts +62 -73
  17. package/dist/index.js +71 -1
  18. package/dist/index.mjs +71 -1
  19. package/dist/jobs-DAGAVHQL.d.ts +105 -0
  20. package/dist/jobs-DhePKSRe.d.mts +105 -0
  21. package/dist/{salaries-CaGlcCC6.d.ts → salaries-D6SUVMmt.d.mts} +2 -1
  22. package/dist/{salaries-01g4wK8y.d.mts → salaries-cqb78kg0.d.ts} +2 -1
  23. package/dist/search-BalPAS0P.d.ts +81 -0
  24. package/dist/search-DYUQzCq_.d.mts +81 -0
  25. package/dist/seo.d.mts +4 -3
  26. package/dist/seo.d.ts +4 -3
  27. package/dist/server.d.mts +56 -4
  28. package/dist/server.d.ts +56 -4
  29. package/dist/server.js +41 -0
  30. package/dist/server.mjs +41 -0
  31. package/dist/sitemap.d.mts +5 -3
  32. package/dist/sitemap.d.ts +5 -3
  33. package/dist/suggest.d.mts +70 -0
  34. package/dist/suggest.d.ts +70 -0
  35. package/dist/suggest.js +165 -0
  36. package/dist/suggest.mjs +144 -0
  37. package/dist/theme.d.mts +49 -1
  38. package/dist/theme.d.ts +49 -1
  39. package/dist/theme.js +60 -26
  40. package/dist/theme.mjs +60 -26
  41. package/package.json +11 -1
  42. package/skills/cavuno-board-filters/SKILL.md +11 -4
  43. package/skills/cavuno-board-jobs/SKILL.md +18 -2
  44. package/skills/cavuno-board-suggest/SKILL.md +119 -0
  45. package/skills/manifest.json +9 -2
package/dist/theme.d.mts CHANGED
@@ -40,6 +40,54 @@ interface ThemeInput {
40
40
  * lives in exactly one place.
41
41
  */
42
42
  declare function isSafeThemeColorValue(value: string): boolean;
43
+ /**
44
+ * Email-safe snapshot keys (platform brand snapshot / MIG-04).
45
+ * Light-only subset consumed by Convex email branding.
46
+ */
47
+ type EmailSafeSnapshotColorKey = 'buttonPrimary' | 'buttonPrimaryText' | 'background' | 'mutedBackground' | 'border' | 'text' | 'textMuted' | 'brandColor';
48
+ /**
49
+ * Inverse of the email-safe subset of `tokenLines`: for each snapshot
50
+ * color key, the ordered list of CSS custom property names (no `--`)
51
+ * that may supply it. First PRESENT property wins when DIFFERENT CSS
52
+ * names map to one snapshot key (e.g. muted > secondary for
53
+ * mutedBackground). Within one CSS property name, multi-`:root` cascade
54
+ * is last-wins and is resolved before this table is consulted.
55
+ *
56
+ * Additive export for the platform brand-snapshot deriver (MIG-04) —
57
+ * keeps the CSS ↔ board contract next to its forward twin.
58
+ */
59
+ declare const EMAIL_SAFE_SNAPSHOT_FROM_CSS: {
60
+ readonly [K in EmailSafeSnapshotColorKey]: readonly string[];
61
+ };
62
+ /**
63
+ * Board key → CSS custom property name(s) (no `--` prefix).
64
+ *
65
+ * The forward write contract for the builder theme tab (MIG-05) and the
66
+ * emission table for `boardThemeToCss` / `tokenLines` — one table, so a
67
+ * tab edit and a render path cannot drift. When a board key fans out to
68
+ * several CSS names (e.g. `text` → foreground + card-foreground + …),
69
+ * a single tab edit writes every name with the same hex value.
70
+ *
71
+ * Fallbacks that only apply at *emit* time when a preferred key is
72
+ * absent (`destructive` ← buttonDanger ?? textError; `ring` ← brandColor
73
+ * ?? buttonPrimary) stay in `tokenLines` — they are not owned write
74
+ * targets of the fallback key.
75
+ */
76
+ declare const BOARD_COLOR_TO_CSS_TOKENS: {
77
+ readonly [K in BoardColorKey]: readonly string[];
78
+ };
79
+ /**
80
+ * CSS custom property names (no `--`) that fan out from *optional*
81
+ * ColorTokensSchema keys (`textSubtle`, `textDisabled`). Production
82
+ * starter `tokens.css` may omit these declarations; the builder theme
83
+ * batch lane SKIPS missing optional declarations rather than escalating
84
+ * the whole preset to the model (MIG-05). Required declarations still
85
+ * escalate on absence — never invent (L5-B).
86
+ */
87
+ declare const OPTIONAL_THEME_DECLARATIONS: readonly ["foreground-subtle", "foreground-disabled"];
88
+ type OptionalThemeDeclaration = (typeof OPTIONAL_THEME_DECLARATIONS)[number];
89
+ /** True when a CSS custom property name (with or without `--`) is optional. */
90
+ declare function isOptionalThemeDeclaration(token: string): boolean;
43
91
  /**
44
92
  * Render the board theme as `:root` (+ `.dark`) CSS-variable overrides.
45
93
  * Inject once at the app shell, after the static theme stylesheet.
@@ -67,4 +115,4 @@ declare function themeFontFamily(fontKey: string): string;
67
115
  /** One Google Fonts request covering the sans + heading families. */
68
116
  declare function googleFontsUrl(theme: ThemeInput | null): string | null;
69
117
 
70
- export { BOARD_COLOR_KEYS, type BoardColorKey, THEME_FONT_GOOGLE_FAMILIES, type ThemeInput, boardThemeToCss, googleFontsUrl, isSafeThemeColorValue, themeFontFamily, themeMode };
118
+ export { BOARD_COLOR_KEYS, BOARD_COLOR_TO_CSS_TOKENS, type BoardColorKey, EMAIL_SAFE_SNAPSHOT_FROM_CSS, type EmailSafeSnapshotColorKey, OPTIONAL_THEME_DECLARATIONS, type OptionalThemeDeclaration, THEME_FONT_GOOGLE_FAMILIES, type ThemeInput, boardThemeToCss, googleFontsUrl, isOptionalThemeDeclaration, isSafeThemeColorValue, themeFontFamily, themeMode };
package/dist/theme.d.ts CHANGED
@@ -40,6 +40,54 @@ interface ThemeInput {
40
40
  * lives in exactly one place.
41
41
  */
42
42
  declare function isSafeThemeColorValue(value: string): boolean;
43
+ /**
44
+ * Email-safe snapshot keys (platform brand snapshot / MIG-04).
45
+ * Light-only subset consumed by Convex email branding.
46
+ */
47
+ type EmailSafeSnapshotColorKey = 'buttonPrimary' | 'buttonPrimaryText' | 'background' | 'mutedBackground' | 'border' | 'text' | 'textMuted' | 'brandColor';
48
+ /**
49
+ * Inverse of the email-safe subset of `tokenLines`: for each snapshot
50
+ * color key, the ordered list of CSS custom property names (no `--`)
51
+ * that may supply it. First PRESENT property wins when DIFFERENT CSS
52
+ * names map to one snapshot key (e.g. muted > secondary for
53
+ * mutedBackground). Within one CSS property name, multi-`:root` cascade
54
+ * is last-wins and is resolved before this table is consulted.
55
+ *
56
+ * Additive export for the platform brand-snapshot deriver (MIG-04) —
57
+ * keeps the CSS ↔ board contract next to its forward twin.
58
+ */
59
+ declare const EMAIL_SAFE_SNAPSHOT_FROM_CSS: {
60
+ readonly [K in EmailSafeSnapshotColorKey]: readonly string[];
61
+ };
62
+ /**
63
+ * Board key → CSS custom property name(s) (no `--` prefix).
64
+ *
65
+ * The forward write contract for the builder theme tab (MIG-05) and the
66
+ * emission table for `boardThemeToCss` / `tokenLines` — one table, so a
67
+ * tab edit and a render path cannot drift. When a board key fans out to
68
+ * several CSS names (e.g. `text` → foreground + card-foreground + …),
69
+ * a single tab edit writes every name with the same hex value.
70
+ *
71
+ * Fallbacks that only apply at *emit* time when a preferred key is
72
+ * absent (`destructive` ← buttonDanger ?? textError; `ring` ← brandColor
73
+ * ?? buttonPrimary) stay in `tokenLines` — they are not owned write
74
+ * targets of the fallback key.
75
+ */
76
+ declare const BOARD_COLOR_TO_CSS_TOKENS: {
77
+ readonly [K in BoardColorKey]: readonly string[];
78
+ };
79
+ /**
80
+ * CSS custom property names (no `--`) that fan out from *optional*
81
+ * ColorTokensSchema keys (`textSubtle`, `textDisabled`). Production
82
+ * starter `tokens.css` may omit these declarations; the builder theme
83
+ * batch lane SKIPS missing optional declarations rather than escalating
84
+ * the whole preset to the model (MIG-05). Required declarations still
85
+ * escalate on absence — never invent (L5-B).
86
+ */
87
+ declare const OPTIONAL_THEME_DECLARATIONS: readonly ["foreground-subtle", "foreground-disabled"];
88
+ type OptionalThemeDeclaration = (typeof OPTIONAL_THEME_DECLARATIONS)[number];
89
+ /** True when a CSS custom property name (with or without `--`) is optional. */
90
+ declare function isOptionalThemeDeclaration(token: string): boolean;
43
91
  /**
44
92
  * Render the board theme as `:root` (+ `.dark`) CSS-variable overrides.
45
93
  * Inject once at the app shell, after the static theme stylesheet.
@@ -67,4 +115,4 @@ declare function themeFontFamily(fontKey: string): string;
67
115
  /** One Google Fonts request covering the sans + heading families. */
68
116
  declare function googleFontsUrl(theme: ThemeInput | null): string | null;
69
117
 
70
- export { BOARD_COLOR_KEYS, type BoardColorKey, THEME_FONT_GOOGLE_FAMILIES, type ThemeInput, boardThemeToCss, googleFontsUrl, isSafeThemeColorValue, themeFontFamily, themeMode };
118
+ export { BOARD_COLOR_KEYS, BOARD_COLOR_TO_CSS_TOKENS, type BoardColorKey, EMAIL_SAFE_SNAPSHOT_FROM_CSS, type EmailSafeSnapshotColorKey, OPTIONAL_THEME_DECLARATIONS, type OptionalThemeDeclaration, THEME_FONT_GOOGLE_FAMILIES, type ThemeInput, boardThemeToCss, googleFontsUrl, isOptionalThemeDeclaration, isSafeThemeColorValue, themeFontFamily, themeMode };
package/dist/theme.js CHANGED
@@ -21,9 +21,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var theme_exports = {};
22
22
  __export(theme_exports, {
23
23
  BOARD_COLOR_KEYS: () => BOARD_COLOR_KEYS,
24
+ BOARD_COLOR_TO_CSS_TOKENS: () => BOARD_COLOR_TO_CSS_TOKENS,
25
+ EMAIL_SAFE_SNAPSHOT_FROM_CSS: () => EMAIL_SAFE_SNAPSHOT_FROM_CSS,
26
+ OPTIONAL_THEME_DECLARATIONS: () => OPTIONAL_THEME_DECLARATIONS,
24
27
  THEME_FONT_GOOGLE_FAMILIES: () => THEME_FONT_GOOGLE_FAMILIES,
25
28
  boardThemeToCss: () => boardThemeToCss,
26
29
  googleFontsUrl: () => googleFontsUrl,
30
+ isOptionalThemeDeclaration: () => isOptionalThemeDeclaration,
27
31
  isSafeThemeColorValue: () => isSafeThemeColorValue,
28
32
  themeFontFamily: () => themeFontFamily,
29
33
  themeMode: () => themeMode
@@ -56,37 +60,67 @@ function color(colors, key) {
56
60
  if (typeof value !== "string" || !value) return void 0;
57
61
  return isSafeThemeColorValue(value) ? value : void 0;
58
62
  }
63
+ var EMAIL_SAFE_SNAPSHOT_FROM_CSS = {
64
+ buttonPrimary: ["primary"],
65
+ buttonPrimaryText: ["primary-foreground"],
66
+ background: ["background"],
67
+ // muted wins; secondary fills only when --muted is absent (A3).
68
+ mutedBackground: ["muted", "secondary"],
69
+ border: ["border"],
70
+ text: ["foreground"],
71
+ textMuted: ["muted-foreground"],
72
+ brandColor: ["ring"]
73
+ };
74
+ var BOARD_COLOR_TO_CSS_TOKENS = {
75
+ background: ["background"],
76
+ text: [
77
+ "foreground",
78
+ "card-foreground",
79
+ "popover-foreground",
80
+ "secondary-foreground",
81
+ "accent-foreground"
82
+ ],
83
+ surfaceBackground: ["card", "popover"],
84
+ mutedBackground: ["secondary", "muted"],
85
+ highlightBackground: ["accent"],
86
+ buttonPrimary: ["primary"],
87
+ buttonPrimaryText: ["primary-foreground"],
88
+ buttonDanger: ["destructive"],
89
+ textMuted: ["muted-foreground"],
90
+ border: ["border", "input"],
91
+ brandColor: ["ring"],
92
+ contrastBackground: ["contrast-background"],
93
+ contrastText: ["contrast-foreground"],
94
+ textSubtle: ["foreground-subtle"],
95
+ textDisabled: ["foreground-disabled"],
96
+ textError: ["foreground-error"]
97
+ };
98
+ var OPTIONAL_THEME_DECLARATIONS = [
99
+ "foreground-subtle",
100
+ "foreground-disabled"
101
+ ];
102
+ function isOptionalThemeDeclaration(token) {
103
+ const name = token.startsWith("--") ? token.slice(2) : token;
104
+ return OPTIONAL_THEME_DECLARATIONS.includes(name);
105
+ }
59
106
  function tokenLines(colors) {
60
107
  const lines = [];
61
108
  const set = (token, value) => {
62
109
  if (value) lines.push(` --${token}: ${value};`);
63
110
  };
64
- set("background", color(colors, "background"));
65
- set("foreground", color(colors, "text"));
66
- set("card", color(colors, "surfaceBackground"));
67
- set("card-foreground", color(colors, "text"));
68
- set("popover", color(colors, "surfaceBackground"));
69
- set("popover-foreground", color(colors, "text"));
70
- set("primary", color(colors, "buttonPrimary"));
71
- set("primary-foreground", color(colors, "buttonPrimaryText"));
72
- set("secondary", color(colors, "mutedBackground"));
73
- set("secondary-foreground", color(colors, "text"));
74
- set("muted", color(colors, "mutedBackground"));
75
- set("muted-foreground", color(colors, "textMuted"));
76
- set("accent", color(colors, "highlightBackground"));
77
- set("accent-foreground", color(colors, "text"));
78
- set(
79
- "destructive",
80
- color(colors, "buttonDanger") ?? color(colors, "textError")
81
- );
82
- set("border", color(colors, "border"));
83
- set("input", color(colors, "border"));
84
- set("ring", color(colors, "brandColor") ?? color(colors, "buttonPrimary"));
85
- set("contrast-background", color(colors, "contrastBackground"));
86
- set("contrast-foreground", color(colors, "contrastText"));
87
- set("foreground-subtle", color(colors, "textSubtle"));
88
- set("foreground-disabled", color(colors, "textDisabled"));
89
- set("foreground-error", color(colors, "textError"));
111
+ for (const boardKey of BOARD_COLOR_KEYS) {
112
+ const value = color(colors, boardKey);
113
+ if (!value) continue;
114
+ for (const cssToken of BOARD_COLOR_TO_CSS_TOKENS[boardKey]) {
115
+ set(cssToken, value);
116
+ }
117
+ }
118
+ if (!color(colors, "buttonDanger") && color(colors, "textError")) {
119
+ set("destructive", color(colors, "textError"));
120
+ }
121
+ if (!color(colors, "brandColor") && color(colors, "buttonPrimary")) {
122
+ set("ring", color(colors, "buttonPrimary"));
123
+ }
90
124
  return lines;
91
125
  }
92
126
  function boardThemeToCss(theme) {
package/dist/theme.mjs CHANGED
@@ -26,37 +26,67 @@ function color(colors, key) {
26
26
  if (typeof value !== "string" || !value) return void 0;
27
27
  return isSafeThemeColorValue(value) ? value : void 0;
28
28
  }
29
+ var EMAIL_SAFE_SNAPSHOT_FROM_CSS = {
30
+ buttonPrimary: ["primary"],
31
+ buttonPrimaryText: ["primary-foreground"],
32
+ background: ["background"],
33
+ // muted wins; secondary fills only when --muted is absent (A3).
34
+ mutedBackground: ["muted", "secondary"],
35
+ border: ["border"],
36
+ text: ["foreground"],
37
+ textMuted: ["muted-foreground"],
38
+ brandColor: ["ring"]
39
+ };
40
+ var BOARD_COLOR_TO_CSS_TOKENS = {
41
+ background: ["background"],
42
+ text: [
43
+ "foreground",
44
+ "card-foreground",
45
+ "popover-foreground",
46
+ "secondary-foreground",
47
+ "accent-foreground"
48
+ ],
49
+ surfaceBackground: ["card", "popover"],
50
+ mutedBackground: ["secondary", "muted"],
51
+ highlightBackground: ["accent"],
52
+ buttonPrimary: ["primary"],
53
+ buttonPrimaryText: ["primary-foreground"],
54
+ buttonDanger: ["destructive"],
55
+ textMuted: ["muted-foreground"],
56
+ border: ["border", "input"],
57
+ brandColor: ["ring"],
58
+ contrastBackground: ["contrast-background"],
59
+ contrastText: ["contrast-foreground"],
60
+ textSubtle: ["foreground-subtle"],
61
+ textDisabled: ["foreground-disabled"],
62
+ textError: ["foreground-error"]
63
+ };
64
+ var OPTIONAL_THEME_DECLARATIONS = [
65
+ "foreground-subtle",
66
+ "foreground-disabled"
67
+ ];
68
+ function isOptionalThemeDeclaration(token) {
69
+ const name = token.startsWith("--") ? token.slice(2) : token;
70
+ return OPTIONAL_THEME_DECLARATIONS.includes(name);
71
+ }
29
72
  function tokenLines(colors) {
30
73
  const lines = [];
31
74
  const set = (token, value) => {
32
75
  if (value) lines.push(` --${token}: ${value};`);
33
76
  };
34
- set("background", color(colors, "background"));
35
- set("foreground", color(colors, "text"));
36
- set("card", color(colors, "surfaceBackground"));
37
- set("card-foreground", color(colors, "text"));
38
- set("popover", color(colors, "surfaceBackground"));
39
- set("popover-foreground", color(colors, "text"));
40
- set("primary", color(colors, "buttonPrimary"));
41
- set("primary-foreground", color(colors, "buttonPrimaryText"));
42
- set("secondary", color(colors, "mutedBackground"));
43
- set("secondary-foreground", color(colors, "text"));
44
- set("muted", color(colors, "mutedBackground"));
45
- set("muted-foreground", color(colors, "textMuted"));
46
- set("accent", color(colors, "highlightBackground"));
47
- set("accent-foreground", color(colors, "text"));
48
- set(
49
- "destructive",
50
- color(colors, "buttonDanger") ?? color(colors, "textError")
51
- );
52
- set("border", color(colors, "border"));
53
- set("input", color(colors, "border"));
54
- set("ring", color(colors, "brandColor") ?? color(colors, "buttonPrimary"));
55
- set("contrast-background", color(colors, "contrastBackground"));
56
- set("contrast-foreground", color(colors, "contrastText"));
57
- set("foreground-subtle", color(colors, "textSubtle"));
58
- set("foreground-disabled", color(colors, "textDisabled"));
59
- set("foreground-error", color(colors, "textError"));
77
+ for (const boardKey of BOARD_COLOR_KEYS) {
78
+ const value = color(colors, boardKey);
79
+ if (!value) continue;
80
+ for (const cssToken of BOARD_COLOR_TO_CSS_TOKENS[boardKey]) {
81
+ set(cssToken, value);
82
+ }
83
+ }
84
+ if (!color(colors, "buttonDanger") && color(colors, "textError")) {
85
+ set("destructive", color(colors, "textError"));
86
+ }
87
+ if (!color(colors, "brandColor") && color(colors, "buttonPrimary")) {
88
+ set("ring", color(colors, "buttonPrimary"));
89
+ }
60
90
  return lines;
61
91
  }
62
92
  function boardThemeToCss(theme) {
@@ -131,9 +161,13 @@ function googleFontsUrl(theme) {
131
161
  }
132
162
  export {
133
163
  BOARD_COLOR_KEYS,
164
+ BOARD_COLOR_TO_CSS_TOKENS,
165
+ EMAIL_SAFE_SNAPSHOT_FROM_CSS,
166
+ OPTIONAL_THEME_DECLARATIONS,
134
167
  THEME_FONT_GOOGLE_FAMILIES,
135
168
  boardThemeToCss,
136
169
  googleFontsUrl,
170
+ isOptionalThemeDeclaration,
137
171
  isSafeThemeColorValue,
138
172
  themeFontFamily,
139
173
  themeMode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavuno/board",
3
- "version": "1.38.0",
3
+ "version": "1.40.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
- accepts repeated params or a comma-string and normalizes with the hosted
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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.38.0",
2
+ "version": "1.40.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.",