@cavuno/board 1.39.0 → 1.41.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/dist/{_spec-DxC1ze93.d.mts → _spec-l_596ZwW.d.mts} +373 -0
  2. package/dist/{_spec-DxC1ze93.d.ts → _spec-l_596ZwW.d.ts} +373 -0
  3. package/dist/bin.mjs +242 -18
  4. package/dist/{board-RfZEAJse.d.ts → board-BzK5j73O.d.ts} +1 -1
  5. package/dist/{board-CqYibYUA.d.mts → board-CyHA6rRP.d.mts} +1 -1
  6. package/dist/doctor.js +238 -14
  7. package/dist/doctor.mjs +238 -14
  8. package/dist/filters.d.mts +2 -2
  9. package/dist/filters.d.ts +2 -2
  10. package/dist/format.d.mts +3 -3
  11. package/dist/format.d.ts +3 -3
  12. package/dist/index.d.mts +89 -10
  13. package/dist/index.d.ts +89 -10
  14. package/dist/index.js +91 -1
  15. package/dist/index.mjs +91 -1
  16. package/dist/{jobs-DPPA1Nev.d.mts → jobs-BhthvMkA.d.mts} +1 -1
  17. package/dist/{jobs-CLLIvtMc.d.ts → jobs-CXudz1Y4.d.ts} +1 -1
  18. package/dist/{salaries-DK4RnJnw.d.ts → salaries-ByBuHCLH.d.ts} +2 -2
  19. package/dist/{salaries-Rb5h_eVZ.d.mts → salaries-Dr_Zugal.d.mts} +2 -2
  20. package/dist/{search-CqBa1Qc4.d.mts → search-CrIlXSJP.d.mts} +1 -1
  21. package/dist/{search-DBoMM-gE.d.ts → search-DZrq-k76.d.ts} +1 -1
  22. package/dist/seo.d.mts +4 -4
  23. package/dist/seo.d.ts +4 -4
  24. package/dist/server.d.mts +56 -6
  25. package/dist/server.d.ts +56 -6
  26. package/dist/server.js +41 -0
  27. package/dist/server.mjs +41 -0
  28. package/dist/sitemap.d.mts +5 -5
  29. package/dist/sitemap.d.ts +5 -5
  30. package/dist/suggest.d.mts +2 -2
  31. package/dist/suggest.d.ts +2 -2
  32. package/dist/theme.d.mts +49 -1
  33. package/dist/theme.d.ts +49 -1
  34. package/dist/theme.js +60 -26
  35. package/dist/theme.mjs +60 -26
  36. package/package.json +1 -1
  37. package/skills/manifest.json +1 -1
package/dist/server.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './_spec-DxC1ze93.js';
3
- import './search-DBoMM-gE.js';
4
- import './board-RfZEAJse.js';
5
- import './jobs-CLLIvtMc.js';
6
- import './salaries-DK4RnJnw.js';
2
+ import './_spec-l_596ZwW.js';
3
+ import './search-DZrq-k76.js';
4
+ import './board-BzK5j73O.js';
5
+ import './jobs-CXudz1Y4.js';
6
+ import './salaries-ByBuHCLH.js';
7
7
 
8
8
  /**
9
9
  * Session cookie codec — pure (no framework imports, no node imports) so it
@@ -77,6 +77,56 @@ declare function safeRedirectPath(path: string | undefined | null, defaultPath?:
77
77
  */
78
78
  declare function currentPathFromReferer(referer: string | null): string;
79
79
 
80
+ /**
81
+ * MIG-10 (added scope) — keep `<slug>.cavuno.app` live after a custom-domain
82
+ * cutover by 308-redirecting to the board's canonical custom domain.
83
+ *
84
+ * ## Where this lives (DEVIATION LOUDLY)
85
+ * **Serving-side, not the dispatch edge.** Production hostname→worker
86
+ * routing is the WFP dispatch Worker's KV map (`hostname → workerName`
87
+ * only — see `docs/recipes/ops/wfp-dispatch-custom-domain.md` and
88
+ * `createWfpReleaseDeploy` / `deterministicWorkerName`). That edge has
89
+ * **no board domain data** (no Convex, no primaryDomain). Inventing a
90
+ * second edge datastore for canonical hosts is forbidden by the ticket.
91
+ *
92
+ * Data source: the board's public context already carries
93
+ * `primaryDomain: string | null` (`GET /v1/boards/:id` via
94
+ * `serializeBoardContext`). The starter loads context for every request
95
+ * (theme, features, analytics) — reuse that field. When `primaryDomain`
96
+ * is set, a request whose Host is a `*.cavuno.app` serving host 308s to
97
+ * `https://<primaryDomain><path+query>` in **one hop**. Domainless boards
98
+ * (`primaryDomain === null`) keep **serving** from cavuno.app (no redirect).
99
+ *
100
+ * Wire this helper into the starter's root middleware / entry (the
101
+ * migration starter's host check) with `Response.redirect(url, 308)`.
102
+ * Pure so unit tests pin behaviour without a Worker runtime.
103
+ *
104
+ * Pattern: Vercel `*.vercel.app` / GitHub `github.io` permanent fallback
105
+ * origin — SEO-neutral 308 to the canonical production host.
106
+ */
107
+ /**
108
+ * True when the request host is a cavuno.app board-serving host
109
+ * (slug or board-hash subdomain), not the apex and not a preview host
110
+ * we deliberately leave alone.
111
+ */
112
+ declare function isCavunoAppServingHost(hostname: string): boolean;
113
+ /**
114
+ * When a custom-domain board is hit on its cavuno.app fallback origin,
115
+ * return the one-hop 308 Location to the canonical custom domain.
116
+ * Domainless boards (no primaryDomain) return null — serve in place.
117
+ */
118
+ declare function getCavunoAppCanonicalRedirectUrl(params: {
119
+ currentHost: string | null;
120
+ /**
121
+ * From `board.context().primaryDomain` — the board's active primary
122
+ * custom domain hostname, or null when domainless.
123
+ */
124
+ primaryDomain: string | null | undefined;
125
+ /** Full request URL or path+query; path+query is preserved on the hop. */
126
+ requestUrl?: string | null;
127
+ defaultPath?: string;
128
+ }): string | null;
129
+
80
130
  /**
81
131
  * Single-flight session refresh — dedupes concurrent refreshes for the same
82
132
  * session WITHIN one process/isolate (the rotation race, ADR-0057 wart #4,
@@ -111,4 +161,4 @@ declare function currentPathFromReferer(referer: string | null): string;
111
161
  */
112
162
  declare function createSessionRefresher(board: Pick<BoardSdk, 'auth' | 'client'>): (session: BoardSession) => Promise<BoardSession | null>;
113
163
 
114
- export { BOARD_ACCESS_COOKIE_NAME, type BoardSession, SESSION_COOKIE_NAME, clearGrantCookie, clearSessionCookie, createSessionRefresher, currentPathFromReferer, grantCookieName, isExpiringSoon, parseGrantCookie, parseSessionCookie, safeRedirectPath, serializeGrantCookie, serializeSessionCookie, sessionCookieName };
164
+ export { BOARD_ACCESS_COOKIE_NAME, type BoardSession, SESSION_COOKIE_NAME, clearGrantCookie, clearSessionCookie, createSessionRefresher, currentPathFromReferer, getCavunoAppCanonicalRedirectUrl, grantCookieName, isCavunoAppServingHost, isExpiringSoon, parseGrantCookie, parseSessionCookie, safeRedirectPath, serializeGrantCookie, serializeSessionCookie, sessionCookieName };
package/dist/server.js CHANGED
@@ -26,7 +26,9 @@ __export(server_exports, {
26
26
  clearSessionCookie: () => clearSessionCookie,
27
27
  createSessionRefresher: () => createSessionRefresher,
28
28
  currentPathFromReferer: () => currentPathFromReferer,
29
+ getCavunoAppCanonicalRedirectUrl: () => getCavunoAppCanonicalRedirectUrl,
29
30
  grantCookieName: () => grantCookieName,
31
+ isCavunoAppServingHost: () => isCavunoAppServingHost,
30
32
  isExpiringSoon: () => isExpiringSoon,
31
33
  parseGrantCookie: () => parseGrantCookie,
32
34
  parseSessionCookie: () => parseSessionCookie,
@@ -154,6 +156,45 @@ function currentPathFromReferer(referer) {
154
156
  }
155
157
  }
156
158
 
159
+ // src/server/canonical-host-redirect.ts
160
+ var CAVUNO_APP_SERVING_SUFFIX = ".cavuno.app";
161
+ function normalizeHostname(host) {
162
+ if (!host) return "";
163
+ return host.toLowerCase().split(":")[0] ?? "";
164
+ }
165
+ function extractPathAndSearch(url) {
166
+ if (!url) return "/";
167
+ try {
168
+ const parsed = new URL(url, "https://placeholder.invalid");
169
+ return `${parsed.pathname}${parsed.search}` || "/";
170
+ } catch {
171
+ return "/";
172
+ }
173
+ }
174
+ function isCavunoAppServingHost(hostname) {
175
+ const h = hostname.toLowerCase();
176
+ if (!h.endsWith(CAVUNO_APP_SERVING_SUFFIX)) return false;
177
+ if (h === "cavuno.app") return false;
178
+ if (h.startsWith("preview-")) return false;
179
+ return true;
180
+ }
181
+ function getCavunoAppCanonicalRedirectUrl(params) {
182
+ const { currentHost, primaryDomain, requestUrl, defaultPath = "/" } = params;
183
+ const canonical = primaryDomain?.trim().toLowerCase().replace(/\.+$/, "");
184
+ if (!canonical) {
185
+ return null;
186
+ }
187
+ const hostname = normalizeHostname(currentHost);
188
+ if (!hostname || !isCavunoAppServingHost(hostname)) {
189
+ return null;
190
+ }
191
+ if (hostname === canonical) {
192
+ return null;
193
+ }
194
+ const path = requestUrl != null && requestUrl !== "" ? extractPathAndSearch(requestUrl) : defaultPath;
195
+ return `https://${canonical}${path.startsWith("/") ? path : `/${path}`}`;
196
+ }
197
+
157
198
  // src/errors.ts
158
199
  function isBoardApiError(e) {
159
200
  return e instanceof Error && e.name === "BoardApiError" && typeof e.status === "number" && typeof e.code === "string";
package/dist/server.mjs CHANGED
@@ -115,6 +115,45 @@ function currentPathFromReferer(referer) {
115
115
  }
116
116
  }
117
117
 
118
+ // src/server/canonical-host-redirect.ts
119
+ var CAVUNO_APP_SERVING_SUFFIX = ".cavuno.app";
120
+ function normalizeHostname(host) {
121
+ if (!host) return "";
122
+ return host.toLowerCase().split(":")[0] ?? "";
123
+ }
124
+ function extractPathAndSearch(url) {
125
+ if (!url) return "/";
126
+ try {
127
+ const parsed = new URL(url, "https://placeholder.invalid");
128
+ return `${parsed.pathname}${parsed.search}` || "/";
129
+ } catch {
130
+ return "/";
131
+ }
132
+ }
133
+ function isCavunoAppServingHost(hostname) {
134
+ const h = hostname.toLowerCase();
135
+ if (!h.endsWith(CAVUNO_APP_SERVING_SUFFIX)) return false;
136
+ if (h === "cavuno.app") return false;
137
+ if (h.startsWith("preview-")) return false;
138
+ return true;
139
+ }
140
+ function getCavunoAppCanonicalRedirectUrl(params) {
141
+ const { currentHost, primaryDomain, requestUrl, defaultPath = "/" } = params;
142
+ const canonical = primaryDomain?.trim().toLowerCase().replace(/\.+$/, "");
143
+ if (!canonical) {
144
+ return null;
145
+ }
146
+ const hostname = normalizeHostname(currentHost);
147
+ if (!hostname || !isCavunoAppServingHost(hostname)) {
148
+ return null;
149
+ }
150
+ if (hostname === canonical) {
151
+ return null;
152
+ }
153
+ const path = requestUrl != null && requestUrl !== "" ? extractPathAndSearch(requestUrl) : defaultPath;
154
+ return `https://${canonical}${path.startsWith("/") ? path : `/${path}`}`;
155
+ }
156
+
118
157
  // src/errors.ts
119
158
  function isBoardApiError(e) {
120
159
  return e instanceof Error && e.name === "BoardApiError" && typeof e.status === "number" && typeof e.code === "string";
@@ -172,7 +211,9 @@ export {
172
211
  clearSessionCookie,
173
212
  createSessionRefresher,
174
213
  currentPathFromReferer,
214
+ getCavunoAppCanonicalRedirectUrl,
175
215
  grantCookieName,
216
+ isCavunoAppServingHost,
176
217
  isExpiringSoon,
177
218
  parseGrantCookie,
178
219
  parseSessionCookie,
@@ -1,9 +1,9 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './_spec-DxC1ze93.mjs';
3
- import './search-CqBa1Qc4.mjs';
4
- import './board-CqYibYUA.mjs';
5
- import './jobs-DPPA1Nev.mjs';
6
- import './salaries-Rb5h_eVZ.mjs';
2
+ import './_spec-l_596ZwW.mjs';
3
+ import './search-CrIlXSJP.mjs';
4
+ import './board-CyHA6rRP.mjs';
5
+ import './jobs-BhthvMkA.mjs';
6
+ import './salaries-Dr_Zugal.mjs';
7
7
 
8
8
  /**
9
9
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
package/dist/sitemap.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './_spec-DxC1ze93.js';
3
- import './search-DBoMM-gE.js';
4
- import './board-RfZEAJse.js';
5
- import './jobs-CLLIvtMc.js';
6
- import './salaries-DK4RnJnw.js';
2
+ import './_spec-l_596ZwW.js';
3
+ import './search-DZrq-k76.js';
4
+ import './board-BzK5j73O.js';
5
+ import './jobs-CXudz1Y4.js';
6
+ import './salaries-ByBuHCLH.js';
7
7
 
8
8
  /**
9
9
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
@@ -1,5 +1,5 @@
1
- import { S as SuggestionItem, a as SearchSuggestQuery, F as FetchOptions, b as SuggestResult } from './search-CqBa1Qc4.mjs';
2
- import './_spec-DxC1ze93.mjs';
1
+ import { S as SuggestionItem, a as SearchSuggestQuery, F as FetchOptions, b as SuggestResult } from './search-CrIlXSJP.mjs';
2
+ import './_spec-l_596ZwW.mjs';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/suggest` — framework-agnostic headless search-suggest
package/dist/suggest.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SuggestionItem, a as SearchSuggestQuery, F as FetchOptions, b as SuggestResult } from './search-DBoMM-gE.js';
2
- import './_spec-DxC1ze93.js';
1
+ import { S as SuggestionItem, a as SearchSuggestQuery, F as FetchOptions, b as SuggestResult } from './search-DZrq-k76.js';
2
+ import './_spec-l_596ZwW.js';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/suggest` — framework-agnostic headless search-suggest
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.39.0",
3
+ "version": "1.41.0",
4
4
  "description": "Typed isomorphic client for the Cavuno Board API",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.39.0",
2
+ "version": "1.41.0",
3
3
  "skills": [
4
4
  {
5
5
  "name": "cavuno-board-account",