@cavuno/board 2.0.5 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +1 -3
- package/dist/{_spec-BKtVFcV9.d.mts → _spec-nkWAraS0.d.mts} +4 -18
- package/dist/{_spec-BKtVFcV9.d.ts → _spec-nkWAraS0.d.ts} +4 -18
- package/dist/bin.mjs +40 -57
- package/dist/{board-3Xj_Y-Tx.d.ts → board-NE98PlwN.d.ts} +2 -3
- package/dist/{board-pNN710Zc.d.mts → board-muu8BI8P.d.mts} +2 -3
- package/dist/doctor.js +21 -47
- package/dist/doctor.mjs +24 -50
- package/dist/filters.d.mts +2 -2
- package/dist/filters.d.ts +2 -2
- package/dist/format.d.mts +3 -3
- package/dist/format.d.ts +3 -3
- package/dist/index.d.mts +11 -27
- package/dist/index.d.ts +11 -27
- package/dist/index.js +3 -3
- package/dist/index.mjs +3 -3
- package/dist/{jobs-C4qRfCdP.d.ts → jobs-BiEvL9d7.d.ts} +1 -1
- package/dist/{jobs-k84C0v_8.d.mts → jobs-lVvKCBCU.d.mts} +1 -1
- package/dist/{salaries--J5mTylN.d.ts → salaries-BahBpxWT.d.ts} +2 -2
- package/dist/{salaries-C9YLO5NG.d.mts → salaries-ChSqDBAM.d.mts} +2 -2
- package/dist/{search-CE_qQiM7.d.mts → search-CCyFWPi3.d.mts} +1 -1
- package/dist/{search-CLRQ_El9.d.ts → search-CyRvVMUp.d.ts} +1 -1
- package/dist/seo.d.mts +4 -4
- package/dist/seo.d.ts +4 -4
- package/dist/server.d.mts +5 -5
- package/dist/server.d.ts +5 -5
- package/dist/sitemap.d.mts +5 -5
- package/dist/sitemap.d.ts +5 -5
- package/dist/skills.d.mts +15 -4
- package/dist/skills.d.ts +15 -4
- package/dist/skills.js +30 -7
- package/dist/skills.mjs +30 -8
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/package.json +1 -11
- package/skills/cavuno-board-account/SKILL.md +54 -66
- package/skills/cavuno-board-api-client/SKILL.md +74 -0
- package/skills/cavuno-board-applications/SKILL.md +38 -52
- package/skills/cavuno-board-auth/SKILL.md +36 -61
- package/skills/cavuno-board-blog/SKILL.md +60 -49
- package/skills/cavuno-board-companies/SKILL.md +71 -53
- package/skills/cavuno-board-errors/SKILL.md +29 -46
- package/skills/cavuno-board-filters/SKILL.md +64 -75
- package/skills/cavuno-board-format/SKILL.md +63 -68
- package/skills/cavuno-board-i18n/SKILL.md +33 -62
- package/skills/cavuno-board-job-alerts/SKILL.md +68 -57
- package/skills/cavuno-board-jobs/SKILL.md +63 -55
- package/skills/cavuno-board-messaging/SKILL.md +53 -59
- package/skills/cavuno-board-paywall/SKILL.md +48 -54
- package/skills/cavuno-board-post-a-job/SKILL.md +118 -0
- package/skills/cavuno-board-salaries/SKILL.md +73 -50
- package/skills/cavuno-board-search-suggestions/SKILL.md +82 -0
- package/skills/cavuno-board-seo/BLOG.md +40 -0
- package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +49 -0
- package/skills/cavuno-board-seo/LISTING.md +36 -0
- package/skills/cavuno-board-seo/SALARY.md +61 -0
- package/skills/cavuno-board-seo/SKILL.md +32 -163
- package/skills/cavuno-board-server-sessions/SKILL.md +143 -0
- package/skills/cavuno-board-setup/SKILL.md +43 -80
- package/skills/cavuno-board-sitemap/SKILL.md +76 -93
- package/skills/cavuno-board-smoke-test/SKILL.md +34 -69
- package/skills/manifest.json +43 -57
- package/dist/theme.d.mts +0 -115
- package/dist/theme.d.ts +0 -115
- package/dist/theme.js +0 -195
- package/dist/theme.mjs +0 -174
- package/skills/cavuno-board-client/SKILL.md +0 -89
- package/skills/cavuno-board-job-posting/SKILL.md +0 -116
- package/skills/cavuno-board-server/SKILL.md +0 -229
- package/skills/cavuno-board-suggest/SKILL.md +0 -119
- package/skills/cavuno-board-theme/SKILL.md +0 -57
- package/skills/flavors/tanstack-start/SKILL.md +0 -102
package/dist/theme.d.mts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@cavuno/board/theme` — board theme → shadcn CSS-variable overrides
|
|
3
|
-
*.
|
|
4
|
-
*
|
|
5
|
-
* The board's stored theme (16 semantic colors × light/dark from
|
|
6
|
-
* `board.context().theme`) is mapped onto the canonical shadcn token
|
|
7
|
-
* vocabulary and emitted as overrides of the app's theme block. One
|
|
8
|
-
* contract, two consumers: the dashboard edits the board theme, and agents
|
|
9
|
-
* restyle through the standard shadcn theme file — both end up in the same
|
|
10
|
-
* tokens. The hosted board renders the same 16 source colors through its
|
|
11
|
-
* own token system; a coverage golden asserts neither side
|
|
12
|
-
* silently drops a color key.
|
|
13
|
-
*
|
|
14
|
-
* A null theme emits nothing — the app's static default theme applies
|
|
15
|
-
* untouched.
|
|
16
|
-
*/
|
|
17
|
-
/** The 16 board color keys (the wire ships them as an open record). */
|
|
18
|
-
declare const BOARD_COLOR_KEYS: readonly ["brandColor", "buttonPrimary", "buttonPrimaryText", "buttonDanger", "background", "surfaceBackground", "mutedBackground", "highlightBackground", "text", "textSubtle", "textMuted", "textDisabled", "textError", "border", "contrastBackground", "contrastText"];
|
|
19
|
-
type BoardColorKey = (typeof BOARD_COLOR_KEYS)[number];
|
|
20
|
-
/**
|
|
21
|
-
* Structural input — satisfied by the SDK's `PublicBoardTheme` (whose color
|
|
22
|
-
* records are open on the wire) and any narrowed server copy.
|
|
23
|
-
*/
|
|
24
|
-
interface ThemeInput {
|
|
25
|
-
mode: string;
|
|
26
|
-
schemeId: string;
|
|
27
|
-
typography: {
|
|
28
|
-
fontSans: string;
|
|
29
|
-
fontHeading?: string | null;
|
|
30
|
-
};
|
|
31
|
-
colors: {
|
|
32
|
-
light?: Record<string, unknown>;
|
|
33
|
-
dark?: Record<string, unknown>;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* True when `value` is safe to emit inside a `<style>` CSS declaration — i.e.
|
|
38
|
-
* carries none of the breakout characters above. Shared by the SDK render sink
|
|
39
|
-
* (`boardThemeToCss`) and the hosted public-API serializer, so the denylist
|
|
40
|
-
* lives in exactly one place.
|
|
41
|
-
*/
|
|
42
|
-
declare function isSafeThemeColorValue(value: string): boolean;
|
|
43
|
-
/**
|
|
44
|
-
* Email-safe snapshot keys.
|
|
45
|
-
* Light-only subset consumed by server-rendered 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 —
|
|
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 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 (without `--`) backed by optional color tokens.
|
|
81
|
-
* A starter may omit these declarations; required declarations must still
|
|
82
|
-
* be present.
|
|
83
|
-
*/
|
|
84
|
-
declare const OPTIONAL_THEME_DECLARATIONS: readonly ["foreground-subtle", "foreground-disabled"];
|
|
85
|
-
type OptionalThemeDeclaration = (typeof OPTIONAL_THEME_DECLARATIONS)[number];
|
|
86
|
-
/** True when a CSS custom property name (with or without `--`) is optional. */
|
|
87
|
-
declare function isOptionalThemeDeclaration(token: string): boolean;
|
|
88
|
-
/**
|
|
89
|
-
* Render the board theme as `:root` (+ `.dark`) CSS-variable overrides.
|
|
90
|
-
* Inject once at the app shell, after the static theme stylesheet.
|
|
91
|
-
*
|
|
92
|
-
* @example
|
|
93
|
-
* const css = boardThemeToCss((await board.context()).theme);
|
|
94
|
-
*/
|
|
95
|
-
declare function boardThemeToCss(theme: ThemeInput | null): string;
|
|
96
|
-
/** The board's color-scheme preference; unknown/absent → `system`. */
|
|
97
|
-
declare function themeMode(theme: ThemeInput | null): 'light' | 'dark' | 'system';
|
|
98
|
-
/**
|
|
99
|
-
* Theme font key → Google Fonts family slug, transcribed from the hosted
|
|
100
|
-
* board's `theme-fonts-metadata.ts` (tested against it). The wire's
|
|
101
|
-
* `typography.fontSans` is an internal KEY (`'source-sans-pro'`), and the
|
|
102
|
-
* Google family is not always a re-casing of it (`source-sans-pro` →
|
|
103
|
-
* `Source+Sans+3`) — never build a font request from the raw key.
|
|
104
|
-
*/
|
|
105
|
-
declare const THEME_FONT_GOOGLE_FAMILIES: Record<string, string>;
|
|
106
|
-
/**
|
|
107
|
-
* The CSS `font-family` name the Google stylesheet registers for a font key
|
|
108
|
-
* — the name `--font-sans` must reference for the loaded font to apply
|
|
109
|
-
* (e.g. `'source-sans-pro'` → `"Source Sans 3"`).
|
|
110
|
-
*/
|
|
111
|
-
declare function themeFontFamily(fontKey: string): string;
|
|
112
|
-
/** One Google Fonts request covering the sans + heading families. */
|
|
113
|
-
declare function googleFontsUrl(theme: ThemeInput | null): string | null;
|
|
114
|
-
|
|
115
|
-
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
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@cavuno/board/theme` — board theme → shadcn CSS-variable overrides
|
|
3
|
-
*.
|
|
4
|
-
*
|
|
5
|
-
* The board's stored theme (16 semantic colors × light/dark from
|
|
6
|
-
* `board.context().theme`) is mapped onto the canonical shadcn token
|
|
7
|
-
* vocabulary and emitted as overrides of the app's theme block. One
|
|
8
|
-
* contract, two consumers: the dashboard edits the board theme, and agents
|
|
9
|
-
* restyle through the standard shadcn theme file — both end up in the same
|
|
10
|
-
* tokens. The hosted board renders the same 16 source colors through its
|
|
11
|
-
* own token system; a coverage golden asserts neither side
|
|
12
|
-
* silently drops a color key.
|
|
13
|
-
*
|
|
14
|
-
* A null theme emits nothing — the app's static default theme applies
|
|
15
|
-
* untouched.
|
|
16
|
-
*/
|
|
17
|
-
/** The 16 board color keys (the wire ships them as an open record). */
|
|
18
|
-
declare const BOARD_COLOR_KEYS: readonly ["brandColor", "buttonPrimary", "buttonPrimaryText", "buttonDanger", "background", "surfaceBackground", "mutedBackground", "highlightBackground", "text", "textSubtle", "textMuted", "textDisabled", "textError", "border", "contrastBackground", "contrastText"];
|
|
19
|
-
type BoardColorKey = (typeof BOARD_COLOR_KEYS)[number];
|
|
20
|
-
/**
|
|
21
|
-
* Structural input — satisfied by the SDK's `PublicBoardTheme` (whose color
|
|
22
|
-
* records are open on the wire) and any narrowed server copy.
|
|
23
|
-
*/
|
|
24
|
-
interface ThemeInput {
|
|
25
|
-
mode: string;
|
|
26
|
-
schemeId: string;
|
|
27
|
-
typography: {
|
|
28
|
-
fontSans: string;
|
|
29
|
-
fontHeading?: string | null;
|
|
30
|
-
};
|
|
31
|
-
colors: {
|
|
32
|
-
light?: Record<string, unknown>;
|
|
33
|
-
dark?: Record<string, unknown>;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* True when `value` is safe to emit inside a `<style>` CSS declaration — i.e.
|
|
38
|
-
* carries none of the breakout characters above. Shared by the SDK render sink
|
|
39
|
-
* (`boardThemeToCss`) and the hosted public-API serializer, so the denylist
|
|
40
|
-
* lives in exactly one place.
|
|
41
|
-
*/
|
|
42
|
-
declare function isSafeThemeColorValue(value: string): boolean;
|
|
43
|
-
/**
|
|
44
|
-
* Email-safe snapshot keys.
|
|
45
|
-
* Light-only subset consumed by server-rendered 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 —
|
|
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 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 (without `--`) backed by optional color tokens.
|
|
81
|
-
* A starter may omit these declarations; required declarations must still
|
|
82
|
-
* be present.
|
|
83
|
-
*/
|
|
84
|
-
declare const OPTIONAL_THEME_DECLARATIONS: readonly ["foreground-subtle", "foreground-disabled"];
|
|
85
|
-
type OptionalThemeDeclaration = (typeof OPTIONAL_THEME_DECLARATIONS)[number];
|
|
86
|
-
/** True when a CSS custom property name (with or without `--`) is optional. */
|
|
87
|
-
declare function isOptionalThemeDeclaration(token: string): boolean;
|
|
88
|
-
/**
|
|
89
|
-
* Render the board theme as `:root` (+ `.dark`) CSS-variable overrides.
|
|
90
|
-
* Inject once at the app shell, after the static theme stylesheet.
|
|
91
|
-
*
|
|
92
|
-
* @example
|
|
93
|
-
* const css = boardThemeToCss((await board.context()).theme);
|
|
94
|
-
*/
|
|
95
|
-
declare function boardThemeToCss(theme: ThemeInput | null): string;
|
|
96
|
-
/** The board's color-scheme preference; unknown/absent → `system`. */
|
|
97
|
-
declare function themeMode(theme: ThemeInput | null): 'light' | 'dark' | 'system';
|
|
98
|
-
/**
|
|
99
|
-
* Theme font key → Google Fonts family slug, transcribed from the hosted
|
|
100
|
-
* board's `theme-fonts-metadata.ts` (tested against it). The wire's
|
|
101
|
-
* `typography.fontSans` is an internal KEY (`'source-sans-pro'`), and the
|
|
102
|
-
* Google family is not always a re-casing of it (`source-sans-pro` →
|
|
103
|
-
* `Source+Sans+3`) — never build a font request from the raw key.
|
|
104
|
-
*/
|
|
105
|
-
declare const THEME_FONT_GOOGLE_FAMILIES: Record<string, string>;
|
|
106
|
-
/**
|
|
107
|
-
* The CSS `font-family` name the Google stylesheet registers for a font key
|
|
108
|
-
* — the name `--font-sans` must reference for the loaded font to apply
|
|
109
|
-
* (e.g. `'source-sans-pro'` → `"Source Sans 3"`).
|
|
110
|
-
*/
|
|
111
|
-
declare function themeFontFamily(fontKey: string): string;
|
|
112
|
-
/** One Google Fonts request covering the sans + heading families. */
|
|
113
|
-
declare function googleFontsUrl(theme: ThemeInput | null): string | null;
|
|
114
|
-
|
|
115
|
-
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
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
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/theme/index.ts
|
|
21
|
-
var theme_exports = {};
|
|
22
|
-
__export(theme_exports, {
|
|
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,
|
|
27
|
-
THEME_FONT_GOOGLE_FAMILIES: () => THEME_FONT_GOOGLE_FAMILIES,
|
|
28
|
-
boardThemeToCss: () => boardThemeToCss,
|
|
29
|
-
googleFontsUrl: () => googleFontsUrl,
|
|
30
|
-
isOptionalThemeDeclaration: () => isOptionalThemeDeclaration,
|
|
31
|
-
isSafeThemeColorValue: () => isSafeThemeColorValue,
|
|
32
|
-
themeFontFamily: () => themeFontFamily,
|
|
33
|
-
themeMode: () => themeMode
|
|
34
|
-
});
|
|
35
|
-
module.exports = __toCommonJS(theme_exports);
|
|
36
|
-
var BOARD_COLOR_KEYS = [
|
|
37
|
-
"brandColor",
|
|
38
|
-
"buttonPrimary",
|
|
39
|
-
"buttonPrimaryText",
|
|
40
|
-
"buttonDanger",
|
|
41
|
-
"background",
|
|
42
|
-
"surfaceBackground",
|
|
43
|
-
"mutedBackground",
|
|
44
|
-
"highlightBackground",
|
|
45
|
-
"text",
|
|
46
|
-
"textSubtle",
|
|
47
|
-
"textMuted",
|
|
48
|
-
"textDisabled",
|
|
49
|
-
"textError",
|
|
50
|
-
"border",
|
|
51
|
-
"contrastBackground",
|
|
52
|
-
"contrastText"
|
|
53
|
-
];
|
|
54
|
-
var CSS_INJECTION_CHARS = /[<>{};]|[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
55
|
-
function isSafeThemeColorValue(value) {
|
|
56
|
-
return !CSS_INJECTION_CHARS.test(value);
|
|
57
|
-
}
|
|
58
|
-
function color(colors, key) {
|
|
59
|
-
const value = colors[key];
|
|
60
|
-
if (typeof value !== "string" || !value) return void 0;
|
|
61
|
-
return isSafeThemeColorValue(value) ? value : void 0;
|
|
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.
|
|
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
|
-
}
|
|
106
|
-
function tokenLines(colors) {
|
|
107
|
-
const lines = [];
|
|
108
|
-
const set = (token, value) => {
|
|
109
|
-
if (value) lines.push(` --${token}: ${value};`);
|
|
110
|
-
};
|
|
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
|
-
}
|
|
124
|
-
return lines;
|
|
125
|
-
}
|
|
126
|
-
function boardThemeToCss(theme) {
|
|
127
|
-
if (!theme) return "";
|
|
128
|
-
const light = tokenLines(theme.colors.light ?? {});
|
|
129
|
-
const dark = tokenLines(theme.colors.dark ?? {});
|
|
130
|
-
const fontSans = theme.typography?.fontSans;
|
|
131
|
-
const genericStack = "ui-sans-serif, system-ui, sans-serif";
|
|
132
|
-
const sansStack = fontSans ? `'${themeFontFamily(fontSans)}', ${genericStack}` : genericStack;
|
|
133
|
-
if (fontSans) {
|
|
134
|
-
light.push(` --font-sans: ${sansStack};`);
|
|
135
|
-
}
|
|
136
|
-
const fontHeading = theme.typography?.fontHeading;
|
|
137
|
-
if (fontHeading) {
|
|
138
|
-
light.push(
|
|
139
|
-
` --font-heading: '${themeFontFamily(fontHeading)}', ${sansStack};`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
const blocks = [];
|
|
143
|
-
if (light.length > 0) blocks.push(`:root {
|
|
144
|
-
${light.join("\n")}
|
|
145
|
-
}`);
|
|
146
|
-
if (dark.length > 0) blocks.push(`.dark {
|
|
147
|
-
${dark.join("\n")}
|
|
148
|
-
}`);
|
|
149
|
-
return blocks.join("\n\n");
|
|
150
|
-
}
|
|
151
|
-
function themeMode(theme) {
|
|
152
|
-
if (!theme) return "system";
|
|
153
|
-
return theme.mode === "light" || theme.mode === "dark" ? theme.mode : "system";
|
|
154
|
-
}
|
|
155
|
-
var THEME_FONT_GOOGLE_FAMILIES = {
|
|
156
|
-
"be-vietnam-pro": "Be+Vietnam+Pro",
|
|
157
|
-
inter: "Inter",
|
|
158
|
-
"plus-jakarta-sans": "Plus+Jakarta+Sans",
|
|
159
|
-
"dm-sans": "DM+Sans",
|
|
160
|
-
"public-sans": "Public+Sans",
|
|
161
|
-
figtree: "Figtree",
|
|
162
|
-
"work-sans": "Work+Sans",
|
|
163
|
-
"open-sans": "Open+Sans",
|
|
164
|
-
poppins: "Poppins",
|
|
165
|
-
hind: "Hind",
|
|
166
|
-
lexend: "Lexend",
|
|
167
|
-
"fira-sans": "Fira+Sans",
|
|
168
|
-
manrope: "Manrope",
|
|
169
|
-
"source-sans-pro": "Source+Sans+3",
|
|
170
|
-
outfit: "Outfit",
|
|
171
|
-
"space-grotesk": "Space+Grotesk",
|
|
172
|
-
geist: "Geist",
|
|
173
|
-
"source-serif-4": "Source+Serif+4",
|
|
174
|
-
lora: "Lora",
|
|
175
|
-
"crimson-pro": "Crimson+Pro"
|
|
176
|
-
};
|
|
177
|
-
function googleFamily(fontKey) {
|
|
178
|
-
return THEME_FONT_GOOGLE_FAMILIES[fontKey] ?? encodeURIComponent(fontKey).replaceAll("%20", "+");
|
|
179
|
-
}
|
|
180
|
-
function themeFontFamily(fontKey) {
|
|
181
|
-
return googleFamily(fontKey).replaceAll("+", " ");
|
|
182
|
-
}
|
|
183
|
-
function googleFontsUrl(theme) {
|
|
184
|
-
if (!theme) return null;
|
|
185
|
-
const families = /* @__PURE__ */ new Set();
|
|
186
|
-
if (theme.typography?.fontSans) {
|
|
187
|
-
families.add(googleFamily(theme.typography.fontSans));
|
|
188
|
-
}
|
|
189
|
-
if (theme.typography?.fontHeading) {
|
|
190
|
-
families.add(googleFamily(theme.typography.fontHeading));
|
|
191
|
-
}
|
|
192
|
-
if (families.size === 0) return null;
|
|
193
|
-
const params = [...families].map((f) => `family=${f}:wght@400;500;600;700`).join("&");
|
|
194
|
-
return `https://fonts.googleapis.com/css2?${params}&display=swap`;
|
|
195
|
-
}
|
package/dist/theme.mjs
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
// src/theme/index.ts
|
|
2
|
-
var BOARD_COLOR_KEYS = [
|
|
3
|
-
"brandColor",
|
|
4
|
-
"buttonPrimary",
|
|
5
|
-
"buttonPrimaryText",
|
|
6
|
-
"buttonDanger",
|
|
7
|
-
"background",
|
|
8
|
-
"surfaceBackground",
|
|
9
|
-
"mutedBackground",
|
|
10
|
-
"highlightBackground",
|
|
11
|
-
"text",
|
|
12
|
-
"textSubtle",
|
|
13
|
-
"textMuted",
|
|
14
|
-
"textDisabled",
|
|
15
|
-
"textError",
|
|
16
|
-
"border",
|
|
17
|
-
"contrastBackground",
|
|
18
|
-
"contrastText"
|
|
19
|
-
];
|
|
20
|
-
var CSS_INJECTION_CHARS = /[<>{};]|[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
21
|
-
function isSafeThemeColorValue(value) {
|
|
22
|
-
return !CSS_INJECTION_CHARS.test(value);
|
|
23
|
-
}
|
|
24
|
-
function color(colors, key) {
|
|
25
|
-
const value = colors[key];
|
|
26
|
-
if (typeof value !== "string" || !value) return void 0;
|
|
27
|
-
return isSafeThemeColorValue(value) ? value : void 0;
|
|
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.
|
|
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
|
-
}
|
|
72
|
-
function tokenLines(colors) {
|
|
73
|
-
const lines = [];
|
|
74
|
-
const set = (token, value) => {
|
|
75
|
-
if (value) lines.push(` --${token}: ${value};`);
|
|
76
|
-
};
|
|
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
|
-
}
|
|
90
|
-
return lines;
|
|
91
|
-
}
|
|
92
|
-
function boardThemeToCss(theme) {
|
|
93
|
-
if (!theme) return "";
|
|
94
|
-
const light = tokenLines(theme.colors.light ?? {});
|
|
95
|
-
const dark = tokenLines(theme.colors.dark ?? {});
|
|
96
|
-
const fontSans = theme.typography?.fontSans;
|
|
97
|
-
const genericStack = "ui-sans-serif, system-ui, sans-serif";
|
|
98
|
-
const sansStack = fontSans ? `'${themeFontFamily(fontSans)}', ${genericStack}` : genericStack;
|
|
99
|
-
if (fontSans) {
|
|
100
|
-
light.push(` --font-sans: ${sansStack};`);
|
|
101
|
-
}
|
|
102
|
-
const fontHeading = theme.typography?.fontHeading;
|
|
103
|
-
if (fontHeading) {
|
|
104
|
-
light.push(
|
|
105
|
-
` --font-heading: '${themeFontFamily(fontHeading)}', ${sansStack};`
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
const blocks = [];
|
|
109
|
-
if (light.length > 0) blocks.push(`:root {
|
|
110
|
-
${light.join("\n")}
|
|
111
|
-
}`);
|
|
112
|
-
if (dark.length > 0) blocks.push(`.dark {
|
|
113
|
-
${dark.join("\n")}
|
|
114
|
-
}`);
|
|
115
|
-
return blocks.join("\n\n");
|
|
116
|
-
}
|
|
117
|
-
function themeMode(theme) {
|
|
118
|
-
if (!theme) return "system";
|
|
119
|
-
return theme.mode === "light" || theme.mode === "dark" ? theme.mode : "system";
|
|
120
|
-
}
|
|
121
|
-
var THEME_FONT_GOOGLE_FAMILIES = {
|
|
122
|
-
"be-vietnam-pro": "Be+Vietnam+Pro",
|
|
123
|
-
inter: "Inter",
|
|
124
|
-
"plus-jakarta-sans": "Plus+Jakarta+Sans",
|
|
125
|
-
"dm-sans": "DM+Sans",
|
|
126
|
-
"public-sans": "Public+Sans",
|
|
127
|
-
figtree: "Figtree",
|
|
128
|
-
"work-sans": "Work+Sans",
|
|
129
|
-
"open-sans": "Open+Sans",
|
|
130
|
-
poppins: "Poppins",
|
|
131
|
-
hind: "Hind",
|
|
132
|
-
lexend: "Lexend",
|
|
133
|
-
"fira-sans": "Fira+Sans",
|
|
134
|
-
manrope: "Manrope",
|
|
135
|
-
"source-sans-pro": "Source+Sans+3",
|
|
136
|
-
outfit: "Outfit",
|
|
137
|
-
"space-grotesk": "Space+Grotesk",
|
|
138
|
-
geist: "Geist",
|
|
139
|
-
"source-serif-4": "Source+Serif+4",
|
|
140
|
-
lora: "Lora",
|
|
141
|
-
"crimson-pro": "Crimson+Pro"
|
|
142
|
-
};
|
|
143
|
-
function googleFamily(fontKey) {
|
|
144
|
-
return THEME_FONT_GOOGLE_FAMILIES[fontKey] ?? encodeURIComponent(fontKey).replaceAll("%20", "+");
|
|
145
|
-
}
|
|
146
|
-
function themeFontFamily(fontKey) {
|
|
147
|
-
return googleFamily(fontKey).replaceAll("+", " ");
|
|
148
|
-
}
|
|
149
|
-
function googleFontsUrl(theme) {
|
|
150
|
-
if (!theme) return null;
|
|
151
|
-
const families = /* @__PURE__ */ new Set();
|
|
152
|
-
if (theme.typography?.fontSans) {
|
|
153
|
-
families.add(googleFamily(theme.typography.fontSans));
|
|
154
|
-
}
|
|
155
|
-
if (theme.typography?.fontHeading) {
|
|
156
|
-
families.add(googleFamily(theme.typography.fontHeading));
|
|
157
|
-
}
|
|
158
|
-
if (families.size === 0) return null;
|
|
159
|
-
const params = [...families].map((f) => `family=${f}:wght@400;500;600;700`).join("&");
|
|
160
|
-
return `https://fonts.googleapis.com/css2?${params}&display=swap`;
|
|
161
|
-
}
|
|
162
|
-
export {
|
|
163
|
-
BOARD_COLOR_KEYS,
|
|
164
|
-
BOARD_COLOR_TO_CSS_TOKENS,
|
|
165
|
-
EMAIL_SAFE_SNAPSHOT_FROM_CSS,
|
|
166
|
-
OPTIONAL_THEME_DECLARATIONS,
|
|
167
|
-
THEME_FONT_GOOGLE_FAMILIES,
|
|
168
|
-
boardThemeToCss,
|
|
169
|
-
googleFontsUrl,
|
|
170
|
-
isOptionalThemeDeclaration,
|
|
171
|
-
isSafeThemeColorValue,
|
|
172
|
-
themeFontFamily,
|
|
173
|
-
themeMode
|
|
174
|
-
};
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: cavuno-board-client
|
|
3
|
-
description: Create and configure the @cavuno/board client — the pk_ board identifier, optional API-origin override, global headers, hooks, per-call FetchOptions, and the rule that keeps one shared instance safe under SSR.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# The Board API client
|
|
7
|
-
|
|
8
|
-
`createBoardClient` returns a typed client whose namespaces (`jobs`, `companies`, `blog`, `auth`, `me`, …) all route through one request pipeline: board-identifier base path, default headers, bearer token from storage, and your hooks.
|
|
9
|
-
|
|
10
|
-
## When to use
|
|
11
|
-
|
|
12
|
-
- Creating the client instance your whole app shares.
|
|
13
|
-
- Adding global headers, logging, request/response hooks, or framework caching.
|
|
14
|
-
- Calling an endpoint the SDK doesn't expose yet (`client.fetch`).
|
|
15
|
-
|
|
16
|
-
## When not to use
|
|
17
|
-
|
|
18
|
-
- Per-surface usage (listing jobs, auth) — see the surface skills; they assume the client exists.
|
|
19
|
-
|
|
20
|
-
## Create the client
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
import { createBoardClient } from '@cavuno/board';
|
|
24
|
-
|
|
25
|
-
const board = createBoardClient({
|
|
26
|
-
board: 'pk_a8f3...', // pk_ key (preferred) | boards_ id | slug
|
|
27
|
-
});
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Use the `pk_…` publishable key for `board`, not the slug: the slug is operator-mutable and renames break deployed frontends. The `pk_…` key is immutable and client-safe.
|
|
31
|
-
|
|
32
|
-
## Configuration
|
|
33
|
-
|
|
34
|
-
```ts no-check
|
|
35
|
-
const board = createBoardClient({
|
|
36
|
-
board: process.env.PUBLIC_CAVUNO_BOARD!,
|
|
37
|
-
globalHeaders: { 'Accept-Language': 'en' },
|
|
38
|
-
onRequest: async (req) => req, // mutate/replace the request (locale, URL rewrite)
|
|
39
|
-
onResponse: async (res, req) => {}, // side effects only (logging, analytics)
|
|
40
|
-
logger: console,
|
|
41
|
-
auth: { storage: 'memory' }, // see cavuno-board-auth
|
|
42
|
-
});
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
The `onRequest`/`onResponse` hooks are first-class — do not monkey-patch the client. `onResponse` receives a **clone** of the response, so reading its body never disturbs the SDK's own parsing.
|
|
46
|
-
|
|
47
|
-
## Per-call FetchOptions ride straight to fetch
|
|
48
|
-
|
|
49
|
-
Every method takes a trailing `options?` of type `FetchOptions` (`Omit<RequestInit,'body'>` plus `query`). Anything besides `body`/`query` passes through to `fetch` untouched, so framework caching works with zero SDK knowledge:
|
|
50
|
-
|
|
51
|
-
```ts snippet
|
|
52
|
-
// Next.js ISR
|
|
53
|
-
await board.jobs.list({ limit: 20 }, { next: { revalidate: 60, tags: ['jobs'] } });
|
|
54
|
-
// Cloudflare Workers
|
|
55
|
-
await board.jobs.list({ limit: 20 }, { cf: { cacheTtl: 60 } } as never);
|
|
56
|
-
// Standard fetch + abort
|
|
57
|
-
const ac = new AbortController();
|
|
58
|
-
await board.jobs.list({ limit: 20 }, { cache: 'force-cache', signal: ac.signal });
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## One shared instance is SSR-safe — if state stays per-call
|
|
62
|
-
|
|
63
|
-
A single Workers isolate serves many users at once. A module-scoped client is safe **only** while per-user state is passed per call (`options.headers`), not stored on the instance. For browser/SPA usage, the instance may hold the session (`auth.storage`); for SSR, keep the session in an httpOnly cookie and pass `{ headers: { authorization } }` per call (see `cavuno-board-auth`).
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
import { createBoardClient } from '@cavuno/board';
|
|
67
|
-
|
|
68
|
-
// Safe: shared, stateless. Per-user token rides per call.
|
|
69
|
-
export const board = createBoardClient({
|
|
70
|
-
board: process.env.PUBLIC_CAVUNO_BOARD!,
|
|
71
|
-
});
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Escape hatch: client.fetch
|
|
75
|
-
|
|
76
|
-
`board.client.fetch<T>(path, init)` is public and first-class — it runs the full pipeline (base path, headers, token, hooks), not a raw `fetch`. Use it to call an endpoint before the SDK ships a typed method for it:
|
|
77
|
-
|
|
78
|
-
```ts snippet
|
|
79
|
-
const data = await board.client.fetch<{ object: 'list'; data: unknown[] }>(
|
|
80
|
-
'/some-new-endpoint',
|
|
81
|
-
{ query: { limit: 5 } });
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Checklist
|
|
85
|
-
|
|
86
|
-
- [ ] One client created from env values, reused everywhere.
|
|
87
|
-
- [ ] `board` is the `pk_…` key, not the slug.
|
|
88
|
-
- [ ] No per-user state on a shared SSR instance.
|
|
89
|
-
- [ ] Caching done via per-call `FetchOptions`, not a wrapper.
|