@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.
- package/README.md +88 -0
- package/dist/bin.mjs +67 -34
- package/dist/filters.d.mts +55 -0
- package/dist/filters.d.ts +55 -0
- package/dist/filters.js +193 -0
- package/dist/filters.mjs +170 -0
- package/dist/format.d.mts +240 -0
- package/dist/format.d.ts +240 -0
- package/dist/format.js +453 -0
- package/dist/format.mjs +430 -0
- package/dist/index.d.mts +92 -3825
- package/dist/index.d.ts +92 -3825
- package/dist/index.js +154 -7
- package/dist/index.mjs +151 -4
- package/dist/jobs-CM67_J6H.d.mts +3836 -0
- package/dist/jobs-CM67_J6H.d.ts +3836 -0
- package/dist/theme.d.mts +63 -0
- package/dist/theme.d.ts +63 -0
- package/dist/theme.js +149 -0
- package/dist/theme.mjs +128 -0
- package/package.json +37 -2
- package/skills/cavuno-board-account/SKILL.md +147 -0
- package/skills/cavuno-board-applications/SKILL.md +110 -0
- package/skills/cavuno-board-blog/SKILL.md +99 -0
- package/skills/cavuno-board-companies/SKILL.md +99 -0
- package/skills/cavuno-board-filters/SKILL.md +89 -0
- package/skills/cavuno-board-format/SKILL.md +104 -0
- package/skills/cavuno-board-job-alerts/SKILL.md +115 -0
- package/skills/cavuno-board-job-posting/SKILL.md +130 -0
- package/skills/cavuno-board-jobs/SKILL.md +15 -0
- package/skills/cavuno-board-messaging/SKILL.md +121 -0
- package/skills/cavuno-board-paywall/SKILL.md +108 -0
- package/skills/cavuno-board-salaries/SKILL.md +87 -0
- package/skills/cavuno-board-setup/SKILL.md +26 -2
- package/skills/cavuno-board-smoke-test/SKILL.md +84 -0
- package/skills/cavuno-board-theme/SKILL.md +69 -0
- package/skills/manifest.json +92 -1
package/dist/theme.d.mts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cavuno/board/theme` — board theme → shadcn CSS-variable overrides
|
|
3
|
+
* (ADR-0057).
|
|
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 in-monorepo 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
|
+
* Render the board theme as `:root` (+ `.dark`) CSS-variable overrides.
|
|
38
|
+
* Inject once at the app shell, after the static theme stylesheet.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* const css = boardThemeToCss((await board.context()).theme);
|
|
42
|
+
*/
|
|
43
|
+
declare function boardThemeToCss(theme: ThemeInput | null): string;
|
|
44
|
+
/** The board's color-scheme preference; unknown/absent → `system`. */
|
|
45
|
+
declare function themeMode(theme: ThemeInput | null): 'light' | 'dark' | 'system';
|
|
46
|
+
/**
|
|
47
|
+
* Theme font key → Google Fonts family slug, transcribed from the hosted
|
|
48
|
+
* board's `theme-fonts-metadata.ts` (golden-tested against it). The wire's
|
|
49
|
+
* `typography.fontSans` is an internal KEY (`'source-sans-pro'`), and the
|
|
50
|
+
* Google family is not always a re-casing of it (`source-sans-pro` →
|
|
51
|
+
* `Source+Sans+3`) — never build a font request from the raw key.
|
|
52
|
+
*/
|
|
53
|
+
declare const THEME_FONT_GOOGLE_FAMILIES: Record<string, string>;
|
|
54
|
+
/**
|
|
55
|
+
* The CSS `font-family` name the Google stylesheet registers for a font key
|
|
56
|
+
* — the name `--font-sans` must reference for the loaded font to apply
|
|
57
|
+
* (e.g. `'source-sans-pro'` → `"Source Sans 3"`).
|
|
58
|
+
*/
|
|
59
|
+
declare function themeFontFamily(fontKey: string): string;
|
|
60
|
+
/** One Google Fonts request covering the sans + heading families. */
|
|
61
|
+
declare function googleFontsUrl(theme: ThemeInput | null): string | null;
|
|
62
|
+
|
|
63
|
+
export { BOARD_COLOR_KEYS, type BoardColorKey, THEME_FONT_GOOGLE_FAMILIES, type ThemeInput, boardThemeToCss, googleFontsUrl, themeFontFamily, themeMode };
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cavuno/board/theme` — board theme → shadcn CSS-variable overrides
|
|
3
|
+
* (ADR-0057).
|
|
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 in-monorepo 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
|
+
* Render the board theme as `:root` (+ `.dark`) CSS-variable overrides.
|
|
38
|
+
* Inject once at the app shell, after the static theme stylesheet.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* const css = boardThemeToCss((await board.context()).theme);
|
|
42
|
+
*/
|
|
43
|
+
declare function boardThemeToCss(theme: ThemeInput | null): string;
|
|
44
|
+
/** The board's color-scheme preference; unknown/absent → `system`. */
|
|
45
|
+
declare function themeMode(theme: ThemeInput | null): 'light' | 'dark' | 'system';
|
|
46
|
+
/**
|
|
47
|
+
* Theme font key → Google Fonts family slug, transcribed from the hosted
|
|
48
|
+
* board's `theme-fonts-metadata.ts` (golden-tested against it). The wire's
|
|
49
|
+
* `typography.fontSans` is an internal KEY (`'source-sans-pro'`), and the
|
|
50
|
+
* Google family is not always a re-casing of it (`source-sans-pro` →
|
|
51
|
+
* `Source+Sans+3`) — never build a font request from the raw key.
|
|
52
|
+
*/
|
|
53
|
+
declare const THEME_FONT_GOOGLE_FAMILIES: Record<string, string>;
|
|
54
|
+
/**
|
|
55
|
+
* The CSS `font-family` name the Google stylesheet registers for a font key
|
|
56
|
+
* — the name `--font-sans` must reference for the loaded font to apply
|
|
57
|
+
* (e.g. `'source-sans-pro'` → `"Source Sans 3"`).
|
|
58
|
+
*/
|
|
59
|
+
declare function themeFontFamily(fontKey: string): string;
|
|
60
|
+
/** One Google Fonts request covering the sans + heading families. */
|
|
61
|
+
declare function googleFontsUrl(theme: ThemeInput | null): string | null;
|
|
62
|
+
|
|
63
|
+
export { BOARD_COLOR_KEYS, type BoardColorKey, THEME_FONT_GOOGLE_FAMILIES, type ThemeInput, boardThemeToCss, googleFontsUrl, themeFontFamily, themeMode };
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
THEME_FONT_GOOGLE_FAMILIES: () => THEME_FONT_GOOGLE_FAMILIES,
|
|
25
|
+
boardThemeToCss: () => boardThemeToCss,
|
|
26
|
+
googleFontsUrl: () => googleFontsUrl,
|
|
27
|
+
themeFontFamily: () => themeFontFamily,
|
|
28
|
+
themeMode: () => themeMode
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(theme_exports);
|
|
31
|
+
var BOARD_COLOR_KEYS = [
|
|
32
|
+
"brandColor",
|
|
33
|
+
"buttonPrimary",
|
|
34
|
+
"buttonPrimaryText",
|
|
35
|
+
"buttonDanger",
|
|
36
|
+
"background",
|
|
37
|
+
"surfaceBackground",
|
|
38
|
+
"mutedBackground",
|
|
39
|
+
"highlightBackground",
|
|
40
|
+
"text",
|
|
41
|
+
"textSubtle",
|
|
42
|
+
"textMuted",
|
|
43
|
+
"textDisabled",
|
|
44
|
+
"textError",
|
|
45
|
+
"border",
|
|
46
|
+
"contrastBackground",
|
|
47
|
+
"contrastText"
|
|
48
|
+
];
|
|
49
|
+
function color(colors, key) {
|
|
50
|
+
const value = colors[key];
|
|
51
|
+
return typeof value === "string" && value ? value : void 0;
|
|
52
|
+
}
|
|
53
|
+
function tokenLines(colors) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
const set = (token, value) => {
|
|
56
|
+
if (value) lines.push(` --${token}: ${value};`);
|
|
57
|
+
};
|
|
58
|
+
set("background", color(colors, "background"));
|
|
59
|
+
set("foreground", color(colors, "text"));
|
|
60
|
+
set("card", color(colors, "surfaceBackground"));
|
|
61
|
+
set("card-foreground", color(colors, "text"));
|
|
62
|
+
set("popover", color(colors, "surfaceBackground"));
|
|
63
|
+
set("popover-foreground", color(colors, "text"));
|
|
64
|
+
set("primary", color(colors, "buttonPrimary"));
|
|
65
|
+
set("primary-foreground", color(colors, "buttonPrimaryText"));
|
|
66
|
+
set("secondary", color(colors, "mutedBackground"));
|
|
67
|
+
set("secondary-foreground", color(colors, "text"));
|
|
68
|
+
set("muted", color(colors, "mutedBackground"));
|
|
69
|
+
set("muted-foreground", color(colors, "textMuted"));
|
|
70
|
+
set("accent", color(colors, "highlightBackground"));
|
|
71
|
+
set("accent-foreground", color(colors, "text"));
|
|
72
|
+
set(
|
|
73
|
+
"destructive",
|
|
74
|
+
color(colors, "buttonDanger") ?? color(colors, "textError")
|
|
75
|
+
);
|
|
76
|
+
set("border", color(colors, "border"));
|
|
77
|
+
set("input", color(colors, "border"));
|
|
78
|
+
set("ring", color(colors, "brandColor") ?? color(colors, "buttonPrimary"));
|
|
79
|
+
set("contrast-background", color(colors, "contrastBackground"));
|
|
80
|
+
set("contrast-foreground", color(colors, "contrastText"));
|
|
81
|
+
set("foreground-subtle", color(colors, "textSubtle"));
|
|
82
|
+
set("foreground-disabled", color(colors, "textDisabled"));
|
|
83
|
+
set("foreground-error", color(colors, "textError"));
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
86
|
+
function boardThemeToCss(theme) {
|
|
87
|
+
if (!theme) return "";
|
|
88
|
+
const light = tokenLines(theme.colors.light ?? {});
|
|
89
|
+
const dark = tokenLines(theme.colors.dark ?? {});
|
|
90
|
+
const fontSans = theme.typography?.fontSans;
|
|
91
|
+
if (fontSans) {
|
|
92
|
+
light.push(
|
|
93
|
+
` --font-sans: '${themeFontFamily(fontSans)}', ui-sans-serif, system-ui, sans-serif;`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const blocks = [];
|
|
97
|
+
if (light.length > 0) blocks.push(`:root {
|
|
98
|
+
${light.join("\n")}
|
|
99
|
+
}`);
|
|
100
|
+
if (dark.length > 0) blocks.push(`.dark {
|
|
101
|
+
${dark.join("\n")}
|
|
102
|
+
}`);
|
|
103
|
+
return blocks.join("\n\n");
|
|
104
|
+
}
|
|
105
|
+
function themeMode(theme) {
|
|
106
|
+
if (!theme) return "system";
|
|
107
|
+
return theme.mode === "light" || theme.mode === "dark" ? theme.mode : "system";
|
|
108
|
+
}
|
|
109
|
+
var THEME_FONT_GOOGLE_FAMILIES = {
|
|
110
|
+
"be-vietnam-pro": "Be+Vietnam+Pro",
|
|
111
|
+
inter: "Inter",
|
|
112
|
+
"plus-jakarta-sans": "Plus+Jakarta+Sans",
|
|
113
|
+
"dm-sans": "DM+Sans",
|
|
114
|
+
"public-sans": "Public+Sans",
|
|
115
|
+
figtree: "Figtree",
|
|
116
|
+
"work-sans": "Work+Sans",
|
|
117
|
+
"open-sans": "Open+Sans",
|
|
118
|
+
poppins: "Poppins",
|
|
119
|
+
hind: "Hind",
|
|
120
|
+
lexend: "Lexend",
|
|
121
|
+
"fira-sans": "Fira+Sans",
|
|
122
|
+
manrope: "Manrope",
|
|
123
|
+
"source-sans-pro": "Source+Sans+3",
|
|
124
|
+
outfit: "Outfit",
|
|
125
|
+
"space-grotesk": "Space+Grotesk",
|
|
126
|
+
geist: "Geist",
|
|
127
|
+
"source-serif-4": "Source+Serif+4",
|
|
128
|
+
lora: "Lora",
|
|
129
|
+
"crimson-pro": "Crimson+Pro"
|
|
130
|
+
};
|
|
131
|
+
function googleFamily(fontKey) {
|
|
132
|
+
return THEME_FONT_GOOGLE_FAMILIES[fontKey] ?? encodeURIComponent(fontKey).replaceAll("%20", "+");
|
|
133
|
+
}
|
|
134
|
+
function themeFontFamily(fontKey) {
|
|
135
|
+
return googleFamily(fontKey).replaceAll("+", " ");
|
|
136
|
+
}
|
|
137
|
+
function googleFontsUrl(theme) {
|
|
138
|
+
if (!theme) return null;
|
|
139
|
+
const families = /* @__PURE__ */ new Set();
|
|
140
|
+
if (theme.typography?.fontSans) {
|
|
141
|
+
families.add(googleFamily(theme.typography.fontSans));
|
|
142
|
+
}
|
|
143
|
+
if (theme.typography?.fontHeading) {
|
|
144
|
+
families.add(googleFamily(theme.typography.fontHeading));
|
|
145
|
+
}
|
|
146
|
+
if (families.size === 0) return null;
|
|
147
|
+
const params = [...families].map((f) => `family=${f}:wght@400;500;600;700`).join("&");
|
|
148
|
+
return `https://fonts.googleapis.com/css2?${params}&display=swap`;
|
|
149
|
+
}
|
package/dist/theme.mjs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
function color(colors, key) {
|
|
21
|
+
const value = colors[key];
|
|
22
|
+
return typeof value === "string" && value ? value : void 0;
|
|
23
|
+
}
|
|
24
|
+
function tokenLines(colors) {
|
|
25
|
+
const lines = [];
|
|
26
|
+
const set = (token, value) => {
|
|
27
|
+
if (value) lines.push(` --${token}: ${value};`);
|
|
28
|
+
};
|
|
29
|
+
set("background", color(colors, "background"));
|
|
30
|
+
set("foreground", color(colors, "text"));
|
|
31
|
+
set("card", color(colors, "surfaceBackground"));
|
|
32
|
+
set("card-foreground", color(colors, "text"));
|
|
33
|
+
set("popover", color(colors, "surfaceBackground"));
|
|
34
|
+
set("popover-foreground", color(colors, "text"));
|
|
35
|
+
set("primary", color(colors, "buttonPrimary"));
|
|
36
|
+
set("primary-foreground", color(colors, "buttonPrimaryText"));
|
|
37
|
+
set("secondary", color(colors, "mutedBackground"));
|
|
38
|
+
set("secondary-foreground", color(colors, "text"));
|
|
39
|
+
set("muted", color(colors, "mutedBackground"));
|
|
40
|
+
set("muted-foreground", color(colors, "textMuted"));
|
|
41
|
+
set("accent", color(colors, "highlightBackground"));
|
|
42
|
+
set("accent-foreground", color(colors, "text"));
|
|
43
|
+
set(
|
|
44
|
+
"destructive",
|
|
45
|
+
color(colors, "buttonDanger") ?? color(colors, "textError")
|
|
46
|
+
);
|
|
47
|
+
set("border", color(colors, "border"));
|
|
48
|
+
set("input", color(colors, "border"));
|
|
49
|
+
set("ring", color(colors, "brandColor") ?? color(colors, "buttonPrimary"));
|
|
50
|
+
set("contrast-background", color(colors, "contrastBackground"));
|
|
51
|
+
set("contrast-foreground", color(colors, "contrastText"));
|
|
52
|
+
set("foreground-subtle", color(colors, "textSubtle"));
|
|
53
|
+
set("foreground-disabled", color(colors, "textDisabled"));
|
|
54
|
+
set("foreground-error", color(colors, "textError"));
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
function boardThemeToCss(theme) {
|
|
58
|
+
if (!theme) return "";
|
|
59
|
+
const light = tokenLines(theme.colors.light ?? {});
|
|
60
|
+
const dark = tokenLines(theme.colors.dark ?? {});
|
|
61
|
+
const fontSans = theme.typography?.fontSans;
|
|
62
|
+
if (fontSans) {
|
|
63
|
+
light.push(
|
|
64
|
+
` --font-sans: '${themeFontFamily(fontSans)}', ui-sans-serif, system-ui, sans-serif;`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const blocks = [];
|
|
68
|
+
if (light.length > 0) blocks.push(`:root {
|
|
69
|
+
${light.join("\n")}
|
|
70
|
+
}`);
|
|
71
|
+
if (dark.length > 0) blocks.push(`.dark {
|
|
72
|
+
${dark.join("\n")}
|
|
73
|
+
}`);
|
|
74
|
+
return blocks.join("\n\n");
|
|
75
|
+
}
|
|
76
|
+
function themeMode(theme) {
|
|
77
|
+
if (!theme) return "system";
|
|
78
|
+
return theme.mode === "light" || theme.mode === "dark" ? theme.mode : "system";
|
|
79
|
+
}
|
|
80
|
+
var THEME_FONT_GOOGLE_FAMILIES = {
|
|
81
|
+
"be-vietnam-pro": "Be+Vietnam+Pro",
|
|
82
|
+
inter: "Inter",
|
|
83
|
+
"plus-jakarta-sans": "Plus+Jakarta+Sans",
|
|
84
|
+
"dm-sans": "DM+Sans",
|
|
85
|
+
"public-sans": "Public+Sans",
|
|
86
|
+
figtree: "Figtree",
|
|
87
|
+
"work-sans": "Work+Sans",
|
|
88
|
+
"open-sans": "Open+Sans",
|
|
89
|
+
poppins: "Poppins",
|
|
90
|
+
hind: "Hind",
|
|
91
|
+
lexend: "Lexend",
|
|
92
|
+
"fira-sans": "Fira+Sans",
|
|
93
|
+
manrope: "Manrope",
|
|
94
|
+
"source-sans-pro": "Source+Sans+3",
|
|
95
|
+
outfit: "Outfit",
|
|
96
|
+
"space-grotesk": "Space+Grotesk",
|
|
97
|
+
geist: "Geist",
|
|
98
|
+
"source-serif-4": "Source+Serif+4",
|
|
99
|
+
lora: "Lora",
|
|
100
|
+
"crimson-pro": "Crimson+Pro"
|
|
101
|
+
};
|
|
102
|
+
function googleFamily(fontKey) {
|
|
103
|
+
return THEME_FONT_GOOGLE_FAMILIES[fontKey] ?? encodeURIComponent(fontKey).replaceAll("%20", "+");
|
|
104
|
+
}
|
|
105
|
+
function themeFontFamily(fontKey) {
|
|
106
|
+
return googleFamily(fontKey).replaceAll("+", " ");
|
|
107
|
+
}
|
|
108
|
+
function googleFontsUrl(theme) {
|
|
109
|
+
if (!theme) return null;
|
|
110
|
+
const families = /* @__PURE__ */ new Set();
|
|
111
|
+
if (theme.typography?.fontSans) {
|
|
112
|
+
families.add(googleFamily(theme.typography.fontSans));
|
|
113
|
+
}
|
|
114
|
+
if (theme.typography?.fontHeading) {
|
|
115
|
+
families.add(googleFamily(theme.typography.fontHeading));
|
|
116
|
+
}
|
|
117
|
+
if (families.size === 0) return null;
|
|
118
|
+
const params = [...families].map((f) => `family=${f}:wght@400;500;600;700`).join("&");
|
|
119
|
+
return `https://fonts.googleapis.com/css2?${params}&display=swap`;
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
BOARD_COLOR_KEYS,
|
|
123
|
+
THEME_FONT_GOOGLE_FAMILIES,
|
|
124
|
+
boardThemeToCss,
|
|
125
|
+
googleFontsUrl,
|
|
126
|
+
themeFontFamily,
|
|
127
|
+
themeMode
|
|
128
|
+
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cavuno/board",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.0",
|
|
4
4
|
"description": "Typed isomorphic client for the Cavuno Board API",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/wollemiahq/cavuno.git",
|
|
@@ -29,6 +30,36 @@
|
|
|
29
30
|
"default": "./dist/index.js"
|
|
30
31
|
}
|
|
31
32
|
},
|
|
33
|
+
"./format": {
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/format.d.mts",
|
|
36
|
+
"default": "./dist/format.mjs"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/format.d.ts",
|
|
40
|
+
"default": "./dist/format.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"./filters": {
|
|
44
|
+
"import": {
|
|
45
|
+
"types": "./dist/filters.d.mts",
|
|
46
|
+
"default": "./dist/filters.mjs"
|
|
47
|
+
},
|
|
48
|
+
"require": {
|
|
49
|
+
"types": "./dist/filters.d.ts",
|
|
50
|
+
"default": "./dist/filters.js"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"./theme": {
|
|
54
|
+
"import": {
|
|
55
|
+
"types": "./dist/theme.d.mts",
|
|
56
|
+
"default": "./dist/theme.mjs"
|
|
57
|
+
},
|
|
58
|
+
"require": {
|
|
59
|
+
"types": "./dist/theme.d.ts",
|
|
60
|
+
"default": "./dist/theme.js"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
32
63
|
"./skills": {
|
|
33
64
|
"import": {
|
|
34
65
|
"types": "./dist/skills.d.mts",
|
|
@@ -49,19 +80,23 @@
|
|
|
49
80
|
"access": "public"
|
|
50
81
|
},
|
|
51
82
|
"scripts": {
|
|
52
|
-
"build": "tsup",
|
|
83
|
+
"build": "rm -rf dist && tsup",
|
|
53
84
|
"clean": "git clean -xdf .turbo dist node_modules src/generated",
|
|
54
85
|
"typecheck": "tsgo --noEmit && tsgo --noEmit -p tsconfig.node.json",
|
|
55
86
|
"test": "vitest run",
|
|
87
|
+
"check:publish": "publint --strict && attw --pack . --profile node16",
|
|
56
88
|
"gen:types": "tsx scripts/gen-types.ts",
|
|
89
|
+
"check:types": "OPENAPI_URL=spec/openapi.snapshot.json tsx scripts/gen-types.ts && git diff --exit-code -- src/generated/openapi-types.ts",
|
|
57
90
|
"gen:skills-manifest": "tsx scripts/generate-skills-manifest.ts",
|
|
58
91
|
"assert-publish-target": "node -e \"const p=require('./package.json'); if(p.name!=='@cavuno/board'){throw new Error('Refusing to publish: package.json name is '+p.name+', expected @cavuno/board')}; if(p.private){throw new Error('Refusing to publish: package.json has private:true')}\"",
|
|
59
92
|
"prepublishOnly": "pnpm run assert-publish-target && pnpm run gen:skills-manifest && pnpm run build"
|
|
60
93
|
},
|
|
61
94
|
"devDependencies": {
|
|
95
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
62
96
|
"@kit/tsconfig": "workspace:*",
|
|
63
97
|
"@types/node": "catalog:",
|
|
64
98
|
"openapi-typescript": "^7.6.0",
|
|
99
|
+
"publint": "^0.3.12",
|
|
65
100
|
"tsup": "^8.4.0",
|
|
66
101
|
"tsx": "^4.19.2",
|
|
67
102
|
"vitest": "^2.1.9"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cavuno-board-account
|
|
3
|
+
description: Candidate account self-service with the @cavuno/board SDK — me.retrieve/delete, the candidate profile (merge-patch update, handle availability), experience/education CRUD, skills/languages full-replace, avatar upload, resume upload with async parse polling, and notification preferences plus the anonymous token unsubscribe. Use when building account, profile-edit, onboarding, or notification-settings pages for a signed-in board user.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Candidate account self-service
|
|
7
|
+
|
|
8
|
+
Everything the signed-in candidate manages about themselves lives under `board.me`. Every call here requires a board-user bearer token — in the browser the SDK reads it from `auth.storage` after login; on the server pass it per call via `options.headers` (see `cavuno-board-auth` for the httpOnly-cookie pattern). The one exception is the anonymous token unsubscribe at the end.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- Account page: show the signed-in user, delete the account.
|
|
13
|
+
- Profile editor: bio/headline/handle, experience, education, skills, languages, avatar.
|
|
14
|
+
- Resume-driven onboarding (upload → parse → auto-populated profile).
|
|
15
|
+
- Email notification settings + the unsubscribe link in emails.
|
|
16
|
+
|
|
17
|
+
## When not to use
|
|
18
|
+
|
|
19
|
+
- Login/registration/tokens — `cavuno-board-auth`.
|
|
20
|
+
- Applications and saved jobs — `cavuno-board-applications`.
|
|
21
|
+
- The employer facet (`me.companies.*`), messaging (`me.conversations.*`), job alerts (`me.alerts.*`).
|
|
22
|
+
|
|
23
|
+
Out of scope — do not invent exports: the SDK ships no form components, file-picker UI, or upload widgets; the host app owns all UI and cookie plumbing.
|
|
24
|
+
|
|
25
|
+
## Account: retrieve and delete
|
|
26
|
+
|
|
27
|
+
```ts snippet
|
|
28
|
+
const me = await board.me.retrieve(); // the authenticated BoardUser
|
|
29
|
+
await board.me.delete(); // 204 — synchronous, irreversible cascade
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`delete` removes the account and all dependent data (profile, collections, saved jobs, alerts, avatar/resume files). Gate it behind an explicit confirmation.
|
|
33
|
+
|
|
34
|
+
## Profile: retrieve, update, handle availability
|
|
35
|
+
|
|
36
|
+
The profile is a singleton. `update` is a merge-patch — omitted fields stay unchanged.
|
|
37
|
+
|
|
38
|
+
```ts snippet
|
|
39
|
+
const profile = await board.me.profile.retrieve();
|
|
40
|
+
profile.handle; profile.headline; profile.jobSearchStatus;
|
|
41
|
+
|
|
42
|
+
await board.me.profile.update({
|
|
43
|
+
headline: 'Staff Engineer',
|
|
44
|
+
jobSearchStatus: 'open_to_offers',
|
|
45
|
+
profileVisibility: 'public',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const { available } = await board.me.profile.handleAvailable('jane');
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`handleAvailable` is advisory (my current handle counts as available); `update` re-checks on write.
|
|
52
|
+
|
|
53
|
+
## Experience and education: per-entry CRUD
|
|
54
|
+
|
|
55
|
+
Both collections are id-keyed CRUD; updates are merge-patch. `createExperience` requires `title`, `companyName`, `startDate`; `createEducation` requires `institutionName`.
|
|
56
|
+
|
|
57
|
+
```ts snippet
|
|
58
|
+
const { data: entries } = await board.me.profile.listExperience();
|
|
59
|
+
const entry = await board.me.profile.createExperience({
|
|
60
|
+
title: 'Staff Engineer',
|
|
61
|
+
companyName: 'Acme',
|
|
62
|
+
startDate: '2022-01',
|
|
63
|
+
});
|
|
64
|
+
await board.me.profile.updateExperience(entry.id, { endDate: '2025-06' });
|
|
65
|
+
await board.me.profile.deleteExperience(entry.id);
|
|
66
|
+
// Education mirrors this: listEducation / createEducation /
|
|
67
|
+
// updateEducation(id, body) / deleteEducation(id).
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Skills and languages: PUT full-replace
|
|
71
|
+
|
|
72
|
+
`updateSkills` / `updateLanguages` replace the **whole set** — there is no add/remove-one endpoint. Sending only the new item wipes the rest. Round-trip the current list:
|
|
73
|
+
|
|
74
|
+
```ts snippet
|
|
75
|
+
const current = await board.me.profile.listSkills();
|
|
76
|
+
await board.me.profile.updateSkills({
|
|
77
|
+
skills: [...current.data.map((s) => s.name), 'TypeScript'], // ordered names
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await board.me.profile.updateLanguages({
|
|
81
|
+
languages: [{ name: 'English', proficiency: 'native' }],
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Both return the full updated list envelope, so you can render straight from the response.
|
|
86
|
+
|
|
87
|
+
## Avatar upload
|
|
88
|
+
|
|
89
|
+
Pass a `Blob`/`File` — the SDK builds the multipart `FormData` (field `file`). JPEG/PNG/WebP, ≤ 5 MB.
|
|
90
|
+
|
|
91
|
+
```ts snippet
|
|
92
|
+
const { avatarUrl } = await board.me.profile.uploadAvatar(file);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Resume: upload, then poll the parse
|
|
96
|
+
|
|
97
|
+
`upload` starts an **async parse** that auto-populates the profile. It returns the resume with `parseStatus: 'parsing'` — poll `retrieve()` until `parsed` or `failed`, then re-read `profile.*` for the populated fields. Options: `keepResumeOnFile`, `importMode: 'append_only' | 'replace_all'`, `confirmReplaceAll`.
|
|
98
|
+
|
|
99
|
+
```ts snippet
|
|
100
|
+
let resume = await board.me.resume.upload(file, { keepResumeOnFile: true });
|
|
101
|
+
// Bounded poll — a stuck parse job must not hang the UI forever.
|
|
102
|
+
const MAX_POLLS = 30; // ~60s at 2s cadence
|
|
103
|
+
for (let i = 0; resume.parseStatus === 'parsing' && i < MAX_POLLS; i++) {
|
|
104
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
105
|
+
resume = await board.me.resume.retrieve();
|
|
106
|
+
}
|
|
107
|
+
if (resume.parseStatus === 'parsed') {
|
|
108
|
+
const profile = await board.me.profile.retrieve(); // auto-populated
|
|
109
|
+
} else if (resume.parseStatus === 'failed') {
|
|
110
|
+
resume.parseFailureReason; // surface it
|
|
111
|
+
} else {
|
|
112
|
+
// still 'parsing' after the window: show "taking longer than usual",
|
|
113
|
+
// keep the manual profile editor usable — never block on the parse.
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`resume.file` holds the stored file (`url` is a short-lived signed download URL). `parseStatus` is `null` when no parse has run. `board.me.resume.delete()` removes the stored file and withdraws keep-on-file consent — parsed profile fields stay.
|
|
118
|
+
|
|
119
|
+
## Notification preferences + anonymous unsubscribe
|
|
120
|
+
|
|
121
|
+
Two channels today: `messageEmails` and `applicationEmails`. `update` toggles one and returns the full updated set.
|
|
122
|
+
|
|
123
|
+
```ts snippet
|
|
124
|
+
const { data: prefs } = await board.me.notificationPreferences.retrieve();
|
|
125
|
+
await board.me.notificationPreferences.update({
|
|
126
|
+
channel: 'messageEmails',
|
|
127
|
+
subscribed: false,
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The one-click unsubscribe link in emails is **anonymous** — no session; the HMAC token is the authorization. All three fields come from the link's query string; resolves void (204):
|
|
132
|
+
|
|
133
|
+
```ts snippet
|
|
134
|
+
await board.me.notificationPreferences.unsubscribeWithToken({
|
|
135
|
+
boardUserId,
|
|
136
|
+
channel: 'applicationEmails',
|
|
137
|
+
token,
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Verify
|
|
142
|
+
|
|
143
|
+
- [ ] A profile `update` followed by `retrieve` shows the new field values.
|
|
144
|
+
- [ ] Adding a skill preserves the existing ones (the full-replace round-trip is in place).
|
|
145
|
+
- [ ] After `uploadAvatar`, the returned `avatarUrl` renders.
|
|
146
|
+
- [ ] Resume upload reaches `parsed` (or surfaces `failed` + `parseFailureReason`), and the profile shows parsed data.
|
|
147
|
+
- [ ] The email unsubscribe link works in a logged-out browser.
|