@cavuno/board 1.32.0 → 1.33.1

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/dist/bin.mjs CHANGED
@@ -40,6 +40,9 @@ function record(id, tier) {
40
40
  var ENV_API_URL = record("env.api-url", 1);
41
41
  var ENV_BOARD_KEY = record("env.board-key", 1);
42
42
  var PK_RE = /^pk_[0-9a-f]{32}$/;
43
+ function apiBase(apiUrl) {
44
+ return apiUrl.replace(/\/+$/, "");
45
+ }
43
46
  function checkEnv(env) {
44
47
  const results = [];
45
48
  if (!env.apiUrl) {
@@ -307,8 +310,7 @@ async function observeVerificationEmail(fetchImpl, apiKey, recipient, pollMs, ti
307
310
  }
308
311
  async function runWriteProbes(options) {
309
312
  const { fetchImpl } = options;
310
- const origin = new URL(options.env.apiUrl).origin;
311
- const base = `${origin}/v1/boards/${encodeURIComponent(options.env.boardKey)}`;
313
+ const base = `${apiBase(options.env.apiUrl)}/v1/boards/${encodeURIComponent(options.env.boardKey)}`;
312
314
  const nonce = options.nonce ?? `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
313
315
  const email = `doctor+${nonce}@cavuno.com`;
314
316
  const password = `doctor-probe-${nonce}-A1!`;
@@ -429,14 +431,12 @@ var STATIC_API = record("static.api", 1);
429
431
  var STATIC_BOARD = record("static.board", 1);
430
432
  var STATIC_SKILLS = record("static.skills", 1);
431
433
  async function checkApiReachable(fetchImpl, apiUrl) {
432
- let origin;
433
434
  try {
434
- origin = new URL(apiUrl).origin;
435
+ new URL(apiUrl);
435
436
  } catch {
436
437
  return null;
437
438
  }
438
- const spec = await probe(fetchImpl, `${origin}/api/v1/openapi.json`);
439
- const result = spec.ok ? spec : await probe(fetchImpl, `${origin}/v1/openapi.json`);
439
+ const result = await probe(fetchImpl, `${apiBase(apiUrl)}/v1/openapi.json`);
440
440
  if (!result.ok) {
441
441
  return STATIC_API(
442
442
  "fail",
@@ -455,10 +455,9 @@ async function checkApiReachable(fetchImpl, apiUrl) {
455
455
  return STATIC_API("pass", "OpenAPI spec reachable");
456
456
  }
457
457
  async function checkBoardResolves(fetchImpl, env) {
458
- const origin = new URL(env.apiUrl).origin;
459
458
  const result = await probe(
460
459
  fetchImpl,
461
- `${origin}/v1/boards/${encodeURIComponent(env.boardKey)}`
460
+ `${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}`
462
461
  );
463
462
  if (!result.ok) {
464
463
  return {
package/dist/index.d.mts CHANGED
@@ -204,7 +204,7 @@ declare function paginate<Q extends Record<string, unknown>, P extends PageShape
204
204
  * constant because the package is platform-neutral and cannot read
205
205
  * package.json at runtime.
206
206
  */
207
- declare const SDK_VERSION = "1.32.0";
207
+ declare const SDK_VERSION = "1.33.1";
208
208
 
209
209
  type SavedJob = Schemas['SavedJob'];
210
210
  type SavedJobsListQuery = {
package/dist/index.d.ts CHANGED
@@ -204,7 +204,7 @@ declare function paginate<Q extends Record<string, unknown>, P extends PageShape
204
204
  * constant because the package is platform-neutral and cannot read
205
205
  * package.json at runtime.
206
206
  */
207
- declare const SDK_VERSION = "1.32.0";
207
+ declare const SDK_VERSION = "1.33.1";
208
208
 
209
209
  type SavedJob = Schemas['SavedJob'];
210
210
  type SavedJobsListQuery = {
package/dist/index.js CHANGED
@@ -312,7 +312,7 @@ async function clearSession(storage) {
312
312
  }
313
313
 
314
314
  // src/version.ts
315
- var SDK_VERSION = "1.32.0";
315
+ var SDK_VERSION = "1.33.1";
316
316
 
317
317
  // src/client.ts
318
318
  function isRawBody(body) {
package/dist/index.mjs CHANGED
@@ -265,7 +265,7 @@ async function clearSession(storage) {
265
265
  }
266
266
 
267
267
  // src/version.ts
268
- var SDK_VERSION = "1.32.0";
268
+ var SDK_VERSION = "1.33.1";
269
269
 
270
270
  // src/client.ts
271
271
  function isRawBody(body) {
package/dist/theme.js CHANGED
@@ -94,9 +94,15 @@ function boardThemeToCss(theme) {
94
94
  const light = tokenLines(theme.colors.light ?? {});
95
95
  const dark = tokenLines(theme.colors.dark ?? {});
96
96
  const fontSans = theme.typography?.fontSans;
97
+ const genericStack = "ui-sans-serif, system-ui, sans-serif";
98
+ const sansStack = fontSans ? `'${themeFontFamily(fontSans)}', ${genericStack}` : genericStack;
97
99
  if (fontSans) {
100
+ light.push(` --font-sans: ${sansStack};`);
101
+ }
102
+ const fontHeading = theme.typography?.fontHeading;
103
+ if (fontHeading) {
98
104
  light.push(
99
- ` --font-sans: '${themeFontFamily(fontSans)}', ui-sans-serif, system-ui, sans-serif;`
105
+ ` --font-heading: '${themeFontFamily(fontHeading)}', ${sansStack};`
100
106
  );
101
107
  }
102
108
  const blocks = [];
package/dist/theme.mjs CHANGED
@@ -64,9 +64,15 @@ function boardThemeToCss(theme) {
64
64
  const light = tokenLines(theme.colors.light ?? {});
65
65
  const dark = tokenLines(theme.colors.dark ?? {});
66
66
  const fontSans = theme.typography?.fontSans;
67
+ const genericStack = "ui-sans-serif, system-ui, sans-serif";
68
+ const sansStack = fontSans ? `'${themeFontFamily(fontSans)}', ${genericStack}` : genericStack;
67
69
  if (fontSans) {
70
+ light.push(` --font-sans: ${sansStack};`);
71
+ }
72
+ const fontHeading = theme.typography?.fontHeading;
73
+ if (fontHeading) {
68
74
  light.push(
69
- ` --font-sans: '${themeFontFamily(fontSans)}', ui-sans-serif, system-ui, sans-serif;`
75
+ ` --font-heading: '${themeFontFamily(fontHeading)}', ${sansStack};`
70
76
  );
71
77
  }
72
78
  const blocks = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavuno/board",
3
- "version": "1.32.0",
3
+ "version": "1.33.1",
4
4
  "description": "Typed isomorphic client for the Cavuno Board API",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: cavuno-board-i18n
3
+ description: Multi-language chrome for a board frontend — the single-language board model, the uiCopy catalog ⊕ operator labels ⊕ generated copy layering, the one copy seam, and how to add path-prefixed locales (/de/, /fr/) with Paraglide JS on TanStack Start. Use when localizing a starter's chrome, adding a language, or wiring locale routing. Do NOT use to translate job content — that stays the board's language.
4
+ ---
5
+
6
+ # Board i18n: localize the chrome, never the content
7
+
8
+ A Cavuno board has **one content language** (`board.context().language`).
9
+ Job titles, descriptions, and company text are that single language and
10
+ never translated by the frontend. What you localize is the **chrome** —
11
+ labels, headings, FAQ scaffolding, filter vocabulary — plus the entity
12
+ data the API already translates (taxonomy, places, salary names).
13
+
14
+ So there are two separate things:
15
+ - **Content** (jobs, companies, blog): board-language, API-served, single.
16
+ - **Chrome** (your authored UI strings): can be multi-locale, path-routed.
17
+
18
+ ## The copy layers (resolve in order)
19
+
20
+ 1. **`uiCopy(language)`** — the versioned SDK catalog (`@cavuno/board/format`,
21
+ `en`/`de`/`fr`, functions-per-key for plurals; no runtime, no provider).
22
+ The floor.
23
+ 2. **`⊕ board.context().labels`** — the operator's per-board overrides,
24
+ API-served (e.g. `featuredLabel` → "Top Job").
25
+ 3. **`⊕ generated code`** — per-board copy the AI builder ejects to native
26
+ code (ADR-0059 layer 3). Supersedes the catalog for that board.
27
+
28
+ Every surface resolves copy through **one seam module** (`src/copy.ts`),
29
+ never by calling the catalog inline:
30
+
31
+ ```ts snippet
32
+ import { uiCopy, type BoardLabelOverrides } from '@cavuno/board/format'
33
+ export function boardCopy(language: string | undefined, labels?: BoardLabelOverrides) {
34
+ return uiCopy(language, labels)
35
+ }
36
+ ```
37
+
38
+ The seam is the **single swap point** for the whole copy lifecycle: catalog
39
+ today → Paraglide messages (below) → builder-generated code later. Components
40
+ never change; only the seam's backing source does.
41
+
42
+ ## Add path-prefixed locales with Paraglide JS (TanStack Start)
43
+
44
+ The starters are TanStack Start, and the TanStack-recommended i18n is
45
+ **Paraglide JS** — compile-time (messages become tree-shakeable functions),
46
+ **no runtime provider** (matches the catalog's no-provider stance), with
47
+ built-in locale routing. The board's language is served unprefixed at `/`;
48
+ extra locales are prefixed (`/de/`, `/fr/`).
49
+
50
+ 1. **Init.** `npx @inlang/paraglide-js@latest init` — creates the inlang
51
+ project, `messages/{en,de,fr}.json`, the `paraglide/` output, and the Vite
52
+ plugin. Add the plugin to `vite.config.ts`.
53
+ 2. **Generate messages FROM `uiCopy` — never hand-translate.** The catalog is
54
+ the source of truth. Emit `messages/{locale}.json` from
55
+ `uiCopy(locale)` so `de`/`fr` come from the reviewed SDK catalog, not a
56
+ second copy. (This generator is the `uiCopy → Paraglide` adapter.)
57
+ 3. **Route.** Wire the router's `rewrite` with Paraglide's `deLocalizeUrl` /
58
+ `localizeUrl` so `/de/jobs` routes internally as `/jobs` and every `Link`
59
+ auto-prefixes the active locale. Default-locale-no-prefix: the board
60
+ language stays at `/`.
61
+ 4. **SSR.** Add `paraglideMiddleware` in the server entry; set `<html lang>`
62
+ from `getLocale()` (it replaces the static `board.language` in the root).
63
+ 5. **Seam.** Point `boardCopy` at the URL locale (`getLocale()`) instead of
64
+ only `board.language`, so `/de/` renders German chrome even on an English
65
+ board. This is the opt-in that ADR-0063 adds over the single-language model.
66
+ 6. **SEO slice.** Per-locale `hreflang`, canonical, and sitemap entries are
67
+ part of completing multi-language — do them when you add real locales, not
68
+ in the foundation.
69
+
70
+ ## Do not
71
+
72
+ - **Do not translate job content** (titles/descriptions). Fully multi-language
73
+ listings is a data-model change, out of scope.
74
+ - **Do not hand-author translations** in the starter — generate messages from
75
+ `uiCopy` so there is one reviewed source.
76
+ - **Do not add a runtime i18n provider** (e.g. use-intl's context). Paraglide
77
+ is compile-time; keep the no-provider property.
78
+ - **Do not scatter catalog calls** through markup — everything imports the one
79
+ seam, so the catalog→code migration is a single-file change.
80
+
81
+ Reference: ADR-0063 (starter i18n), ADR-0059 (copy catalog), ADR-0010 (entity
82
+ translation). Wiring flavor: `cavuno-board-tanstack-start`.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.32.0",
2
+ "version": "1.33.1",
3
3
  "skills": [
4
4
  {
5
5
  "name": "cavuno-board-account",
@@ -64,6 +64,13 @@
64
64
  "framework": null,
65
65
  "category": "core"
66
66
  },
67
+ {
68
+ "name": "cavuno-board-i18n",
69
+ "description": "Multi-language chrome for a board frontend — the single-language board model, the uiCopy catalog ⊕ operator labels ⊕ generated copy layering, the one copy seam, and how to add path-prefixed locales (/de/, /fr/) with Paraglide JS on TanStack Start. Use when localizing a starter's chrome, adding a language, or wiring locale routing. Do NOT use to translate job content — that stays the board's language.",
70
+ "path": "skills/cavuno-board-i18n/SKILL.md",
71
+ "framework": null,
72
+ "category": "core"
73
+ },
67
74
  {
68
75
  "name": "cavuno-board-job-alerts",
69
76
  "description": "Build job-alert flows with the @cavuno/board SDK — the anonymous double-opt-in surface (jobAlerts.subscribe/confirm/resendConfirmation + HMAC-token manage/unsubscribe/resubscribe/updatePreference/deletePreference) and the authenticated board.me.alerts CRUD. Covers which surface to use when, how the manage token rides, which filters actually scope delivery, and the full-replace update trap.",