@cavuno/board 1.24.0 → 1.26.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 (37) hide show
  1. package/README.md +88 -0
  2. package/dist/bin.mjs +67 -34
  3. package/dist/filters.d.mts +55 -0
  4. package/dist/filters.d.ts +55 -0
  5. package/dist/filters.js +193 -0
  6. package/dist/filters.mjs +170 -0
  7. package/dist/format.d.mts +240 -0
  8. package/dist/format.d.ts +240 -0
  9. package/dist/format.js +453 -0
  10. package/dist/format.mjs +430 -0
  11. package/dist/index.d.mts +92 -3825
  12. package/dist/index.d.ts +92 -3825
  13. package/dist/index.js +154 -7
  14. package/dist/index.mjs +151 -4
  15. package/dist/jobs-CM67_J6H.d.mts +3836 -0
  16. package/dist/jobs-CM67_J6H.d.ts +3836 -0
  17. package/dist/theme.d.mts +63 -0
  18. package/dist/theme.d.ts +63 -0
  19. package/dist/theme.js +149 -0
  20. package/dist/theme.mjs +128 -0
  21. package/package.json +37 -2
  22. package/skills/cavuno-board-account/SKILL.md +147 -0
  23. package/skills/cavuno-board-applications/SKILL.md +110 -0
  24. package/skills/cavuno-board-blog/SKILL.md +99 -0
  25. package/skills/cavuno-board-companies/SKILL.md +99 -0
  26. package/skills/cavuno-board-filters/SKILL.md +89 -0
  27. package/skills/cavuno-board-format/SKILL.md +104 -0
  28. package/skills/cavuno-board-job-alerts/SKILL.md +115 -0
  29. package/skills/cavuno-board-job-posting/SKILL.md +130 -0
  30. package/skills/cavuno-board-jobs/SKILL.md +15 -0
  31. package/skills/cavuno-board-messaging/SKILL.md +121 -0
  32. package/skills/cavuno-board-paywall/SKILL.md +108 -0
  33. package/skills/cavuno-board-salaries/SKILL.md +87 -0
  34. package/skills/cavuno-board-setup/SKILL.md +26 -2
  35. package/skills/cavuno-board-smoke-test/SKILL.md +84 -0
  36. package/skills/cavuno-board-theme/SKILL.md +69 -0
  37. package/skills/manifest.json +92 -1
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @cavuno/board
2
+
3
+ Typed, isomorphic client for the [Cavuno](https://cavuno.com) Board API —
4
+ the headless toolkit for building fully-custom job-board frontends. Zero
5
+ dependencies; runs in the browser, Node ≥ 20, and Cloudflare Workers.
6
+
7
+ You bring the framework and own the layout; the SDK brings the job board:
8
+ jobs and search, companies, salaries, blog, board-user auth, saved jobs,
9
+ applications, job alerts, messaging, employer self-service, checkout, and
10
+ the candidate paywall — every method typed from the API's own OpenAPI
11
+ contract.
12
+
13
+ ## Building with a coding agent
14
+
15
+ The package ships an agent skill corpus that teaches Claude Code (or any
16
+ SKILL.md-compatible agent) how to wire a board correctly — client setup,
17
+ auth and session ownership, pagination, gating, error handling, and a
18
+ runtime smoke test:
19
+
20
+ ```bash
21
+ npm install @cavuno/board
22
+ npx cavuno-board setup # copies version-matched skills into .claude/skills and/or .agents/skills
23
+ ```
24
+
25
+ Then ask your agent: *"set up my Cavuno board"* — it reads the
26
+ `cavuno-board-setup` skill and works surface by surface. Because the
27
+ skills live in your project and match your installed version, the agent
28
+ never works from stale docs.
29
+
30
+ ## Quick start (by hand)
31
+
32
+ ```ts
33
+ import { createBoardClient } from '@cavuno/board';
34
+
35
+ const board = createBoardClient({
36
+ baseUrl: process.env.PUBLIC_CAVUNO_API_URL!, // https://api.cavuno.com
37
+ board: process.env.PUBLIC_CAVUNO_BOARD!, // pk_… publishable key
38
+ });
39
+
40
+ const { name, theme, features } = await board.context();
41
+ const page = await board.jobs.list({ limit: 20 });
42
+ const job = await board.jobs.retrieve('senior-chef');
43
+ ```
44
+
45
+ Every method accepts trailing `FetchOptions` — `signal`, `headers`, and
46
+ framework caching directives (`next: { tags }`, `cf: {…}`) pass through to
47
+ `fetch` untouched. Walk full catalogs with the async iterator:
48
+
49
+ ```ts
50
+ import { paginate } from '@cavuno/board';
51
+
52
+ // the query `limit` is the page size; toArray's `limit` caps items collected
53
+ for await (const card of paginate(board.jobs.list, { limit: 100 })) {
54
+ urls.push(card.links.public);
55
+ }
56
+ const first500 = await paginate(board.companies.list, { limit: 100 })
57
+ .toArray({ limit: 500 });
58
+ ```
59
+
60
+ Errors are typed — every non-2xx throws a `BoardApiError` carrying the full
61
+ v1 envelope (`status`, `code`, `details`, `requestId`), with guards like
62
+ `isNotFound`, `isRateLimited`, and `isBoardPasswordRequired`.
63
+
64
+ ## Auth model
65
+
66
+ Three tiers, safe for browsers by construction:
67
+
68
+ - **`pk_…` publishable key** — identifies the board; public by design.
69
+ - **Board-user JWT** — candidate/employer sessions via `board.auth.*`.
70
+ Pluggable storage (`memory` in the browser, `nostore` on the server);
71
+ on SSR, keep the session in an httpOnly cookie your app owns and pass
72
+ the token per call. No auto-refresh on 401 — rotation is explicit.
73
+ - **No secret keys** — operator/admin credentials never touch this SDK.
74
+
75
+ ## Escape hatch
76
+
77
+ `board.client.fetch<T>(path, init)` sends a typed request through the full
78
+ pipeline (board base path, headers, bearer token, hooks) for endpoints the
79
+ SDK doesn't cover yet.
80
+
81
+ ## Docs
82
+
83
+ - API reference & guides: https://cavuno.com/docs/api
84
+ - The OpenAPI document: `GET https://api.cavuno.com/v1/openapi.json`
85
+ - Reference starter (TanStack Start on Cloudflare Workers):
86
+ https://github.com/wollemiahq/cavuno-board-starter
87
+
88
+ MIT © Wollemia
package/dist/bin.mjs CHANGED
@@ -1,4 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __commonJS = (cb, mod) => function __require() {
7
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
8
+ };
2
9
 
3
10
  // src/skills.ts
4
11
  import { readFileSync } from "fs";
@@ -14,6 +21,11 @@ function loadSkillManifest() {
14
21
  const manifestPath = resolveFromPackageRoot("skills/manifest.json");
15
22
  return JSON.parse(readFileSync(manifestPath, "utf8"));
16
23
  }
24
+ var init_skills = __esm({
25
+ "src/skills.ts"() {
26
+ "use strict";
27
+ }
28
+ });
17
29
 
18
30
  // src/setup/run.ts
19
31
  import { cpSync, existsSync, mkdirSync, readFileSync as readFileSync2 } from "fs";
@@ -32,43 +44,64 @@ function runSetup(cwd = process.cwd()) {
32
44
  const chosen = manifest.skills.filter(
33
45
  (skill) => skill.category === "core" || skill.framework === framework
34
46
  );
35
- const targetDir = resolve2(cwd, ".claude", "skills");
36
- mkdirSync(targetDir, { recursive: true });
47
+ const roots = [
48
+ resolve2(cwd, ".claude", "skills"),
49
+ resolve2(cwd, ".agents", "skills")
50
+ ];
51
+ const existing = roots.filter((root) => existsSync(root));
52
+ const targetDirs = existing.length > 0 ? existing : [roots[0]];
37
53
  const copied = [];
38
- for (const skill of chosen) {
39
- const sourceDir = dirname2(resolveFromPackageRoot(skill.path));
40
- const destDir = resolve2(targetDir, skill.name);
41
- mkdirSync(destDir, { recursive: true });
42
- cpSync(sourceDir, destDir, { recursive: true });
43
- copied.push(skill.name);
54
+ for (const targetDir of targetDirs) {
55
+ mkdirSync(targetDir, { recursive: true });
56
+ for (const skill of chosen) {
57
+ const sourceDir = dirname2(resolveFromPackageRoot(skill.path));
58
+ const destDir = resolve2(targetDir, skill.name);
59
+ mkdirSync(destDir, { recursive: true });
60
+ cpSync(sourceDir, destDir, { recursive: true });
61
+ if (!copied.includes(skill.name)) copied.push(skill.name);
62
+ }
44
63
  }
45
- return { version: manifest.version, framework, targetDir, copied };
64
+ return { version: manifest.version, framework, targetDirs, copied };
46
65
  }
66
+ var init_run = __esm({
67
+ "src/setup/run.ts"() {
68
+ "use strict";
69
+ init_skills();
70
+ }
71
+ });
47
72
 
48
73
  // src/bin.ts
49
- function main() {
50
- if (process.argv[2] !== "setup") {
51
- console.error("Usage: cavuno-board setup");
52
- process.exit(1);
53
- }
54
- const result = runSetup();
55
- console.log(`
74
+ var require_bin = __commonJS({
75
+ "src/bin.ts"() {
76
+ init_run();
77
+ function main() {
78
+ if (process.argv[2] !== "setup") {
79
+ console.error("Usage: cavuno-board setup");
80
+ process.exit(1);
81
+ }
82
+ const result = runSetup();
83
+ console.log(`
56
84
  @cavuno/board setup \u2014 v${result.version}`);
57
- console.log(
58
- `Detected framework: ${result.framework ?? "none (core skills only)"}`
59
- );
60
- console.log(`Copied ${result.copied.length} skills \u2192 ${result.targetDir}`);
61
- for (const name of result.copied) console.log(` - ${name}`);
62
- console.log("\nNext steps:");
63
- console.log(" 1. Set your environment variables:");
64
- console.log(" PUBLIC_CAVUNO_API_URL=https://api.cavuno.com");
65
- console.log(
66
- " PUBLIC_CAVUNO_BOARD=pk_... # your board publishable key"
67
- );
68
- console.log(' 2. Ask your coding agent: "set up my Cavuno board".');
69
- console.log(" It reads the cavuno-board-setup skill first.");
70
- console.log(
71
- "\n Re-run `npx @cavuno/board setup` after upgrading to refresh skills."
72
- );
73
- }
74
- main();
85
+ console.log(
86
+ `Detected framework: ${result.framework ?? "none (core skills only)"}`
87
+ );
88
+ console.log(
89
+ `Copied ${result.copied.length} skills \u2192 ${result.targetDirs.join(", ")}`
90
+ );
91
+ for (const name of result.copied) console.log(` - ${name}`);
92
+ console.log("\nNext steps:");
93
+ console.log(" 1. Set your environment variables:");
94
+ console.log(" PUBLIC_CAVUNO_API_URL=https://api.cavuno.com");
95
+ console.log(
96
+ " PUBLIC_CAVUNO_BOARD=pk_... # your board publishable key"
97
+ );
98
+ console.log(' 2. Ask your coding agent: "set up my Cavuno board".');
99
+ console.log(" It reads the cavuno-board-setup skill first.");
100
+ console.log(
101
+ "\n Re-run `npx @cavuno/board setup` after upgrading to refresh skills."
102
+ );
103
+ }
104
+ main();
105
+ }
106
+ });
107
+ export default require_bin();
@@ -0,0 +1,55 @@
1
+ import { l as JobSort, E as EmploymentType, R as RemoteOption, a as Seniority } from './jobs-CM67_J6H.mjs';
2
+
3
+ declare const REMOTE_OPTIONS: readonly RemoteOption[];
4
+ /**
5
+ * The employment types the listing FILTER offers (the hosted filter UI's
6
+ * set) — `volunteer`/`other` exist on the wire but are not filter options.
7
+ */
8
+ declare const EMPLOYMENT_TYPES: readonly EmploymentType[];
9
+ /** All 8 platform seniority levels — the hosted filter renders the full set as a multi-select. */
10
+ declare const SENIORITIES: readonly Seniority[];
11
+ declare const JOB_SORTS: readonly JobSort[];
12
+ declare const DEFAULT_SORT: JobSort;
13
+ /**
14
+ * Seniority display labels in the board language (lexicon-backed, en+de).
15
+ *
16
+ * @example
17
+ * seniorityLabels('de').executive; // "Führungskraft"
18
+ */
19
+ declare function seniorityLabels(locale: string): Record<Seniority, string>;
20
+ /**
21
+ * Sort-dropdown labels. The hosted board has no centralized sort vocabulary
22
+ * yet, so every locale receives these English defaults today. The locale
23
+ * parameter is CONTRACT, not cosmetics: ADR-0057 makes the board language a
24
+ * required leading argument on every label-producing helper so call sites
25
+ * are locale-correct by construction — when a sort vocabulary lands, no
26
+ * consumer changes shape (adding the parameter later would be a breaking
27
+ * change to every call site).
28
+ */
29
+ declare function sortLabels(_locale: string): Record<JobSort, string>;
30
+ interface ListingFilters {
31
+ q?: string;
32
+ remoteOption?: RemoteOption;
33
+ employmentType?: EmploymentType;
34
+ /** Multi-select (parity with the hosted board's seniority checkboxes). */
35
+ seniority?: Seniority[];
36
+ /** Result ordering (ADR-0048); absent ⇒ `relevance`. */
37
+ sort?: JobSort;
38
+ }
39
+ /**
40
+ * Normalise raw seniority input — an array (repeated params) or a
41
+ * comma-string (hand-typed URL) — to valid levels, with the hosted
42
+ * semantics: trim, lowercase, drop unknowns, dedupe preserving order.
43
+ */
44
+ declare function parseSeniority(raw: unknown): Seniority[] | undefined;
45
+ /**
46
+ * Validate raw URL search params into the supported listing filters,
47
+ * dropping unknown values (never throwing — listing URLs are public input).
48
+ *
49
+ * @example
50
+ * parseListingFilters({ seniority: 'senior,lead', sort: 'newest' });
51
+ * // { seniority: ['senior', 'lead'], sort: 'newest' }
52
+ */
53
+ declare function parseListingFilters(search: Record<string, unknown>): ListingFilters;
54
+
55
+ export { DEFAULT_SORT, EMPLOYMENT_TYPES, JOB_SORTS, type ListingFilters, REMOTE_OPTIONS, SENIORITIES, parseListingFilters, parseSeniority, seniorityLabels, sortLabels };
@@ -0,0 +1,55 @@
1
+ import { l as JobSort, E as EmploymentType, R as RemoteOption, a as Seniority } from './jobs-CM67_J6H.js';
2
+
3
+ declare const REMOTE_OPTIONS: readonly RemoteOption[];
4
+ /**
5
+ * The employment types the listing FILTER offers (the hosted filter UI's
6
+ * set) — `volunteer`/`other` exist on the wire but are not filter options.
7
+ */
8
+ declare const EMPLOYMENT_TYPES: readonly EmploymentType[];
9
+ /** All 8 platform seniority levels — the hosted filter renders the full set as a multi-select. */
10
+ declare const SENIORITIES: readonly Seniority[];
11
+ declare const JOB_SORTS: readonly JobSort[];
12
+ declare const DEFAULT_SORT: JobSort;
13
+ /**
14
+ * Seniority display labels in the board language (lexicon-backed, en+de).
15
+ *
16
+ * @example
17
+ * seniorityLabels('de').executive; // "Führungskraft"
18
+ */
19
+ declare function seniorityLabels(locale: string): Record<Seniority, string>;
20
+ /**
21
+ * Sort-dropdown labels. The hosted board has no centralized sort vocabulary
22
+ * yet, so every locale receives these English defaults today. The locale
23
+ * parameter is CONTRACT, not cosmetics: ADR-0057 makes the board language a
24
+ * required leading argument on every label-producing helper so call sites
25
+ * are locale-correct by construction — when a sort vocabulary lands, no
26
+ * consumer changes shape (adding the parameter later would be a breaking
27
+ * change to every call site).
28
+ */
29
+ declare function sortLabels(_locale: string): Record<JobSort, string>;
30
+ interface ListingFilters {
31
+ q?: string;
32
+ remoteOption?: RemoteOption;
33
+ employmentType?: EmploymentType;
34
+ /** Multi-select (parity with the hosted board's seniority checkboxes). */
35
+ seniority?: Seniority[];
36
+ /** Result ordering (ADR-0048); absent ⇒ `relevance`. */
37
+ sort?: JobSort;
38
+ }
39
+ /**
40
+ * Normalise raw seniority input — an array (repeated params) or a
41
+ * comma-string (hand-typed URL) — to valid levels, with the hosted
42
+ * semantics: trim, lowercase, drop unknowns, dedupe preserving order.
43
+ */
44
+ declare function parseSeniority(raw: unknown): Seniority[] | undefined;
45
+ /**
46
+ * Validate raw URL search params into the supported listing filters,
47
+ * dropping unknown values (never throwing — listing URLs are public input).
48
+ *
49
+ * @example
50
+ * parseListingFilters({ seniority: 'senior,lead', sort: 'newest' });
51
+ * // { seniority: ['senior', 'lead'], sort: 'newest' }
52
+ */
53
+ declare function parseListingFilters(search: Record<string, unknown>): ListingFilters;
54
+
55
+ export { DEFAULT_SORT, EMPLOYMENT_TYPES, JOB_SORTS, type ListingFilters, REMOTE_OPTIONS, SENIORITIES, parseListingFilters, parseSeniority, seniorityLabels, sortLabels };
@@ -0,0 +1,193 @@
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/filters/index.ts
21
+ var filters_exports = {};
22
+ __export(filters_exports, {
23
+ DEFAULT_SORT: () => DEFAULT_SORT,
24
+ EMPLOYMENT_TYPES: () => EMPLOYMENT_TYPES,
25
+ JOB_SORTS: () => JOB_SORTS,
26
+ REMOTE_OPTIONS: () => REMOTE_OPTIONS,
27
+ SENIORITIES: () => SENIORITIES,
28
+ parseListingFilters: () => parseListingFilters,
29
+ parseSeniority: () => parseSeniority,
30
+ seniorityLabels: () => seniorityLabels,
31
+ sortLabels: () => sortLabels
32
+ });
33
+ module.exports = __toCommonJS(filters_exports);
34
+
35
+ // src/format/salary-lexicon.ts
36
+ var EN = {
37
+ timeframe: {
38
+ per_year: "Yearly",
39
+ per_month: "Monthly",
40
+ per_week: "Weekly",
41
+ per_day: "Daily",
42
+ per_hour: "Hourly"
43
+ },
44
+ rangePrefix: { from: "From", upTo: "Up to" },
45
+ seniority: {
46
+ entry_level: "Entry level",
47
+ associate: "Associate",
48
+ mid_level: "Mid-level",
49
+ senior: "Senior",
50
+ lead: "Lead",
51
+ principal: "Principal",
52
+ director: "Director",
53
+ executive: "Executive"
54
+ },
55
+ frames: {
56
+ entitySalariesTitle: ({ entity, range }) => `${entity} Salaries (${range}/yr)`,
57
+ salariesInPlaceTitle: ({ place, range }) => `Salaries in ${place} (${range}/yr)`,
58
+ salariesInPlaceTitleNoRange: ({ place }) => `Salaries in ${place}`,
59
+ entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Salaries in ${place} (${range}/yr)`,
60
+ companySalariesTitle: ({ company, range }) => `${company} Salaries${range ? ` (${range}/yr)` : ""}`,
61
+ companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Salaries${range ? ` (${range}/yr)` : ""}`,
62
+ professionalsEarn: ({ entity, range, count }) => `${entity} professionals earn ${range}/yr on average across ${count} job postings.`,
63
+ rolesPay: ({ entity, range, count }) => `${entity} roles pay ${range}/yr on average across ${count} job postings.`,
64
+ professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity} professionals in ${place} earn ${range}/yr on average across ${count} job postings.`,
65
+ seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} roles start at ${lowAmount}, ${highLabel} roles reach ${highAmount}.`,
66
+ seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} salaries start at ${lowAmount}, while ${highLabel} roles reach ${highAmount}.`,
67
+ compareAcrossTopPaying: ({ count }) => `Compare across ${count} top-paying ${count === 1 ? "company" : "companies"}.`,
68
+ compareAcrossTop: ({ count }) => `Compare across ${count} top ${count === 1 ? "company" : "companies"}.`,
69
+ boardWideAverage: ({ entity, range }) => `Board-wide ${entity} average: ${range}/yr.`,
70
+ averageSalaryInCity: ({ place, range, count }) => `Average salary in ${place} is ${range}/yr across ${count} jobs. Browse salary data by title and skill.`,
71
+ browseLocationsInRegion: ({ count, place }) => `Browse salary data across ${count} locations in ${place}. Compare salaries by title, skill, and company.`,
72
+ companyPays: ({ company, range, count }) => `${company} pays ${range}/yr on average across ${count} job postings.`,
73
+ breakdownAcrossCategories: ({ count }) => `Breakdown across ${count} job ${count === 1 ? "category" : "categories"}.`,
74
+ compareWithSimilar: ({ count }) => `Compare with ${count} similar ${count === 1 ? "company" : "companies"}.`,
75
+ salaryInfoFor: ({ company, board }) => `Salary information for ${company} on ${board}.`,
76
+ categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category} roles at ${company} pay ${range}/yr based on ${count} job postings.`,
77
+ categorySalaryDataFor: ({ category, company, board }) => `${category} salary data for ${company} on ${board}.`
78
+ }
79
+ };
80
+ var DE = {
81
+ timeframe: {
82
+ per_year: "pro Jahr",
83
+ per_month: "pro Monat",
84
+ per_week: "pro Woche",
85
+ per_day: "pro Tag",
86
+ per_hour: "pro Stunde"
87
+ },
88
+ rangePrefix: { from: "ab", upTo: "bis zu" },
89
+ seniority: {
90
+ entry_level: "Entry-Level",
91
+ associate: "Associate",
92
+ mid_level: "Mid-Level",
93
+ senior: "Senior",
94
+ lead: "Lead",
95
+ principal: "Principal",
96
+ director: "Director",
97
+ executive: "F\xFChrungskraft"
98
+ },
99
+ frames: {
100
+ entitySalariesTitle: ({ entity, range }) => `${entity} Geh\xE4lter (${range}/Jahr)`,
101
+ salariesInPlaceTitle: ({ place, range }) => `Geh\xE4lter in ${place} (${range}/Jahr)`,
102
+ salariesInPlaceTitleNoRange: ({ place }) => `Geh\xE4lter in ${place}`,
103
+ entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Geh\xE4lter in ${place} (${range}/Jahr)`,
104
+ companySalariesTitle: ({ company, range }) => `${company} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
105
+ companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
106
+ professionalsEarn: ({ entity, range, count }) => `${entity}-Fachkr\xE4fte verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
107
+ rolesPay: ({ entity, range, count }) => `${entity}-Positionen werden mit durchschnittlich ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
108
+ professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity}-Fachkr\xE4fte in ${place} verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
109
+ seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Positionen beginnen bei ${lowAmount}, ${highLabel}-Positionen erreichen ${highAmount}.`,
110
+ seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Geh\xE4lter beginnen bei ${lowAmount}, w\xE4hrend ${highLabel}-Positionen ${highAmount} erreichen.`,
111
+ compareAcrossTopPaying: ({ count }) => `Vergleichen Sie ${count} ${count === 1 ? "bestzahlendes" : "bestzahlende"} Unternehmen.`,
112
+ compareAcrossTop: ({ count }) => `Vergleichen Sie ${count} Top-Unternehmen.`,
113
+ boardWideAverage: ({ entity, range }) => `Gesamtdurchschnitt ${entity}: ${range}/Jahr.`,
114
+ averageSalaryInCity: ({ place, range, count }) => `Das Durchschnittsgehalt in ${place} betr\xE4gt ${range}/Jahr, basierend auf ${count} Stellen. Gehaltsdaten nach Titel und F\xE4higkeiten durchsuchen.`,
115
+ browseLocationsInRegion: ({ count, place }) => `Gehaltsdaten f\xFCr ${count} Standorte in ${place} durchsuchen. Geh\xE4lter nach Titel, F\xE4higkeiten und Unternehmen vergleichen.`,
116
+ companyPays: ({ company, range, count }) => `${company} zahlt durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
117
+ breakdownAcrossCategories: ({ count }) => `Aufschl\xFCsselung \xFCber ${count} Job-${count === 1 ? "Kategorie" : "Kategorien"}.`,
118
+ compareWithSimilar: ({ count }) => `Vergleichen Sie mit ${count} \xE4hnlichen Unternehmen.`,
119
+ salaryInfoFor: ({ company, board }) => `Gehaltsinformationen f\xFCr ${company} auf ${board}.`,
120
+ categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category}-Positionen bei ${company} werden mit ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
121
+ categorySalaryDataFor: ({ category, company, board }) => `${category}-Gehaltsdaten f\xFCr ${company} auf ${board}.`
122
+ }
123
+ };
124
+ var LEXICONS = { en: EN, de: DE };
125
+ function getSalaryLexicon(locale) {
126
+ return locale && LEXICONS[locale] || EN;
127
+ }
128
+
129
+ // src/filters/index.ts
130
+ var REMOTE_OPTIONS = [
131
+ "on_site",
132
+ "hybrid",
133
+ "remote"
134
+ ];
135
+ var EMPLOYMENT_TYPES = [
136
+ "full_time",
137
+ "part_time",
138
+ "contract",
139
+ "internship",
140
+ "temporary"
141
+ ];
142
+ var SENIORITIES = [
143
+ "entry_level",
144
+ "associate",
145
+ "mid_level",
146
+ "senior",
147
+ "lead",
148
+ "principal",
149
+ "director",
150
+ "executive"
151
+ ];
152
+ var JOB_SORTS = [
153
+ "relevance",
154
+ "newest",
155
+ "salary_high"
156
+ ];
157
+ var DEFAULT_SORT = "relevance";
158
+ function seniorityLabels(locale) {
159
+ return getSalaryLexicon(locale).seniority;
160
+ }
161
+ function sortLabels(_locale) {
162
+ return {
163
+ relevance: "AI-ranked",
164
+ newest: "Most recent",
165
+ salary_high: "Salary: high to low"
166
+ };
167
+ }
168
+ var SENIORITY_LOOKUP = new Set(SENIORITIES);
169
+ function parseSeniority(raw) {
170
+ const values = Array.isArray(raw) ? raw : typeof raw === "string" && raw ? raw.split(",") : [];
171
+ const result = [];
172
+ const seen = /* @__PURE__ */ new Set();
173
+ for (const value of values) {
174
+ if (typeof value !== "string") continue;
175
+ const normalized = value.trim().toLowerCase();
176
+ if (!normalized || seen.has(normalized)) continue;
177
+ if (!SENIORITY_LOOKUP.has(normalized)) continue;
178
+ seen.add(normalized);
179
+ result.push(normalized);
180
+ }
181
+ return result.length > 0 ? result : void 0;
182
+ }
183
+ function parseListingFilters(search) {
184
+ return {
185
+ q: typeof search.q === "string" && search.q ? search.q : void 0,
186
+ remoteOption: REMOTE_OPTIONS.includes(search.remoteOption) ? search.remoteOption : void 0,
187
+ employmentType: EMPLOYMENT_TYPES.includes(
188
+ search.employmentType
189
+ ) ? search.employmentType : void 0,
190
+ seniority: parseSeniority(search.seniority),
191
+ sort: JOB_SORTS.includes(search.sort) ? search.sort : void 0
192
+ };
193
+ }