@cavuno/board 1.38.0 → 1.40.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 +7 -0
- package/dist/{jobs-DMH3Ytoq.d.mts → _spec-DRbgPIlN.d.mts} +404 -96
- package/dist/{jobs-DMH3Ytoq.d.ts → _spec-DRbgPIlN.d.ts} +404 -96
- package/dist/bin.mjs +242 -18
- package/dist/{board-CBry4f7H.d.mts → board-BxPUtrOl.d.ts} +1 -1
- package/dist/{board-tV-zcaHs.d.ts → board-DCiNpgFf.d.mts} +1 -1
- package/dist/doctor.js +238 -14
- package/dist/doctor.mjs +238 -14
- package/dist/filters.d.mts +18 -4
- package/dist/filters.d.ts +18 -4
- package/dist/filters.js +17 -0
- package/dist/filters.mjs +17 -0
- package/dist/format.d.mts +3 -2
- package/dist/format.d.ts +3 -2
- package/dist/index.d.mts +62 -73
- package/dist/index.d.ts +62 -73
- package/dist/index.js +71 -1
- package/dist/index.mjs +71 -1
- package/dist/jobs-DAGAVHQL.d.ts +105 -0
- package/dist/jobs-DhePKSRe.d.mts +105 -0
- package/dist/{salaries-CaGlcCC6.d.ts → salaries-D6SUVMmt.d.mts} +2 -1
- package/dist/{salaries-01g4wK8y.d.mts → salaries-cqb78kg0.d.ts} +2 -1
- package/dist/search-BalPAS0P.d.ts +81 -0
- package/dist/search-DYUQzCq_.d.mts +81 -0
- package/dist/seo.d.mts +4 -3
- package/dist/seo.d.ts +4 -3
- package/dist/server.d.mts +56 -4
- package/dist/server.d.ts +56 -4
- package/dist/server.js +41 -0
- package/dist/server.mjs +41 -0
- package/dist/sitemap.d.mts +5 -3
- package/dist/sitemap.d.ts +5 -3
- package/dist/suggest.d.mts +70 -0
- package/dist/suggest.d.ts +70 -0
- package/dist/suggest.js +165 -0
- package/dist/suggest.mjs +144 -0
- package/dist/theme.d.mts +49 -1
- package/dist/theme.d.ts +49 -1
- package/dist/theme.js +60 -26
- package/dist/theme.mjs +60 -26
- package/package.json +11 -1
- package/skills/cavuno-board-filters/SKILL.md +11 -4
- package/skills/cavuno-board-jobs/SKILL.md +18 -2
- package/skills/cavuno-board-suggest/SKILL.md +119 -0
- package/skills/manifest.json +9 -2
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
3
|
+
"version": "1.40.0",
|
|
4
4
|
"description": "Typed isomorphic client for the Cavuno Board API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -50,6 +50,16 @@
|
|
|
50
50
|
"default": "./dist/filters.js"
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
|
+
"./suggest": {
|
|
54
|
+
"import": {
|
|
55
|
+
"types": "./dist/suggest.d.mts",
|
|
56
|
+
"default": "./dist/suggest.mjs"
|
|
57
|
+
},
|
|
58
|
+
"require": {
|
|
59
|
+
"types": "./dist/suggest.d.ts",
|
|
60
|
+
"default": "./dist/suggest.js"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
53
63
|
"./theme": {
|
|
54
64
|
"import": {
|
|
55
65
|
"types": "./dist/theme.d.mts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-filters
|
|
3
|
-
description: The canonical listing-filter vocabulary and URL search-param parsing with @cavuno/board/filters — remote/employment/seniority/sort sets, localized seniority labels, and parseListingFilters for jobs-listing routes. Use when building listing pages, filter sidebars, sort dropdowns, or validating listing URL params.
|
|
3
|
+
description: The canonical listing-filter vocabulary and URL search-param parsing with @cavuno/board/filters — remote/employment/seniority/company/sort sets, localized seniority labels, and parseListingFilters for jobs-listing routes. Use when building listing pages, filter sidebars, sort dropdowns, or validating listing URL params.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Filters: vocabulary + URL parsing
|
|
@@ -21,6 +21,7 @@ never throws).
|
|
|
21
21
|
- Turning values into card display text — `cavuno-board-format`.
|
|
22
22
|
- Search QUERIES (`jobs.search` bodies) — this module is about the listing
|
|
23
23
|
URL contract, not the search POST body.
|
|
24
|
+
- Search-dropdown typeahead — `cavuno-board-suggest`.
|
|
24
25
|
|
|
25
26
|
## Parse listing URLs
|
|
26
27
|
|
|
@@ -28,19 +29,25 @@ never throws).
|
|
|
28
29
|
import { parseListingFilters, DEFAULT_SORT } from '@cavuno/board/filters';
|
|
29
30
|
|
|
30
31
|
const filters = parseListingFilters(rawSearchParams);
|
|
31
|
-
// { q?, remoteOption?, employmentType?, seniority?: Seniority[], sort? }
|
|
32
|
+
// { q?, remoteOption?, employmentType?, seniority?: Seniority[], company?: string[], sort? }
|
|
32
33
|
|
|
33
34
|
const page = await board.jobs.list({
|
|
34
35
|
limit: 20,
|
|
35
36
|
seniority: filters.seniority,
|
|
37
|
+
companySlug: filters.company,
|
|
36
38
|
remoteOption: filters.remoteOption ? [filters.remoteOption] : undefined,
|
|
37
39
|
employmentType: filters.employmentType ? [filters.employmentType] : undefined,
|
|
38
40
|
});
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
Unknown values are dropped silently (public URLs, never throw). Seniority
|
|
42
|
-
|
|
43
|
-
rules: trim, lowercase, dedupe, keep order.
|
|
44
|
+
and `company` accept repeated params or a comma-string and normalize with
|
|
45
|
+
the hosted rules: trim, lowercase, dedupe, keep order. `company` is an open
|
|
46
|
+
value set (public slugs) capped at 10 (wire max; first 10 kept).
|
|
47
|
+
|
|
48
|
+
**Slugs are the URL identity.** Map `filters.company` to the wire as
|
|
49
|
+
`companySlug` (`jobs.list` query / `jobs.search` filters). The API accepts
|
|
50
|
+
slugs directly — never resolve slug→id client-side.
|
|
44
51
|
|
|
45
52
|
## Render the filter UI from the vocabulary
|
|
46
53
|
|
|
@@ -35,7 +35,7 @@ for (const card of page.data) {
|
|
|
35
35
|
|
|
36
36
|
### Filters and pagination
|
|
37
37
|
|
|
38
|
-
`JobsListQuery` supports `limit` (1–100), `offset` (takes precedence over `cursor`), `cursor`, and filters: `companyId`, `remoteOption`, `employmentType`, `seniority` (single or repeated → OR-matched), `location` + `radius` (km), `category`, `skill`. Paginate by passing back `nextCursor`, or use numbered pages with `offset`:
|
|
38
|
+
`JobsListQuery` supports `limit` (1–100), `offset` (takes precedence over `cursor`), `cursor`, and filters: `companyId`, `companySlug`, `remoteOption`, `employmentType`, `seniority` (single or repeated → OR-matched), `location` + `radius` (km), `category`, `skill`. Paginate by passing back `nextCursor`, or use numbered pages with `offset`:
|
|
39
39
|
|
|
40
40
|
```ts snippet
|
|
41
41
|
const p1 = await board.jobs.list({ limit: 20 });
|
|
@@ -46,9 +46,24 @@ const p2 = p1.nextCursor
|
|
|
46
46
|
const page3 = await board.jobs.list({ limit: 20, offset: 40 });
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
### Company filter via public slug
|
|
50
|
+
|
|
51
|
+
`companySlug` is the public URL identity (what listing URLs carry). Prefer it
|
|
52
|
+
over `companyId` in frontends — the API resolves slugs server-side. Unknown
|
|
53
|
+
slugs are **ignored** (they contribute no matches, not an error); an entirely
|
|
54
|
+
unknown set yields an empty result list with `count: 0`. Combined with
|
|
55
|
+
`companyId` as a union when both are set. Cap is 10 values.
|
|
56
|
+
|
|
57
|
+
```ts snippet
|
|
58
|
+
const page = await board.jobs.list({
|
|
59
|
+
limit: 20,
|
|
60
|
+
companySlug: ['acme', 'globex'],
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
49
64
|
## Search
|
|
50
65
|
|
|
51
|
-
`jobs.search` posts a `JobsSearchBody` (free-text `query` + structured `filters`) and returns a `JobCardSearchEnvelope`:
|
|
66
|
+
`jobs.search` posts a `JobsSearchBody` (free-text `query` + structured `filters`) and returns a `JobCardSearchEnvelope`. The same `companySlug` rule applies under `filters`:
|
|
52
67
|
|
|
53
68
|
```ts snippet
|
|
54
69
|
const results = await board.jobs.search({
|
|
@@ -56,6 +71,7 @@ const results = await board.jobs.search({
|
|
|
56
71
|
filters: {
|
|
57
72
|
seniority: ['senior'],
|
|
58
73
|
remoteOption: ['remote'],
|
|
74
|
+
companySlug: ['acme'],
|
|
59
75
|
publishedAt: { gte: '2026-01-01T00:00:00Z' },
|
|
60
76
|
},
|
|
61
77
|
limit: 20,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cavuno-board-suggest
|
|
3
|
+
description: Federated keyword search suggestions with the @cavuno/board SDK — board.search.suggest for companies + taxonomy terms in one server-ranked list, and the headless createSuggestController under @cavuno/board/suggest (debounce, abort, stale-drop). Use when building a search dropdown, typeahead, or autocomplete that mirrors the hosted board.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Search suggest: dropdown typeahead
|
|
7
|
+
|
|
8
|
+
`board.search.suggest` returns ONE interleaved, server-ordered list of
|
|
9
|
+
company and taxonomy-term suggestions for a keyword — the hosted search
|
|
10
|
+
dropdown contract. `@cavuno/board/suggest` wraps that call in a headless
|
|
11
|
+
controller so debounce / abort / stale-drop ship with the SDK and patch via
|
|
12
|
+
npm update.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- The global search input's suggestion dropdown.
|
|
17
|
+
- Any typeahead that should offer companies and category/skill terms
|
|
18
|
+
together, ranked the way the hosted board ranks them.
|
|
19
|
+
|
|
20
|
+
## When not to use
|
|
21
|
+
|
|
22
|
+
- Location autocomplete — `board.taxonomy.places.list({ q })`.
|
|
23
|
+
- Taxonomy-only option lists for filter sidebars —
|
|
24
|
+
`board.taxonomy.categories/skills/suggestions.list`.
|
|
25
|
+
- Job results themselves — `board.jobs.list` / `board.jobs.search`.
|
|
26
|
+
|
|
27
|
+
## Call the endpoint directly
|
|
28
|
+
|
|
29
|
+
```ts snippet
|
|
30
|
+
const { items, query } = await board.search.suggest({ q: 'acme', limit: 10 });
|
|
31
|
+
// items is server-ranked — do NOT re-sort.
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
if (item.type === 'company') {
|
|
34
|
+
item.slug; // public company URL slug
|
|
35
|
+
item.name;
|
|
36
|
+
item.jobCount;
|
|
37
|
+
} else {
|
|
38
|
+
// item.type === 'term'
|
|
39
|
+
item.termType; // 'category' | 'skill'
|
|
40
|
+
item.displayName;
|
|
41
|
+
item.canonicalSlug; // for links
|
|
42
|
+
item.sourceSlug; // for job filters
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Queries under two characters return an empty `items` array. `limit` is
|
|
48
|
+
1–25 (default 25). Order is the contract.
|
|
49
|
+
|
|
50
|
+
## Prefer the headless controller for UI
|
|
51
|
+
|
|
52
|
+
The controller owns debounce (default 250ms), min-chars (default 2),
|
|
53
|
+
in-flight abort, and stale-drop. Compatible with `useSyncExternalStore`.
|
|
54
|
+
|
|
55
|
+
```ts snippet
|
|
56
|
+
import { createSuggestController } from '@cavuno/board/suggest';
|
|
57
|
+
|
|
58
|
+
const suggest = createSuggestController(board, { limit: 10 });
|
|
59
|
+
const unsub = suggest.subscribe(() => {
|
|
60
|
+
const { items, status, query } = suggest.getState();
|
|
61
|
+
// render dropdown; previous items are retained while status === 'loading'
|
|
62
|
+
});
|
|
63
|
+
suggest.setQuery(inputValue);
|
|
64
|
+
// Hide the currently-applied company filter from the list:
|
|
65
|
+
suggest.setExcludedCompanySlugs(appliedCompanySlugs);
|
|
66
|
+
// teardown:
|
|
67
|
+
unsub();
|
|
68
|
+
suggest.dispose();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`setExcludedCompanySlugs` is the ONE permitted view-level filter (company
|
|
72
|
+
items only, re-filters current items synchronously — no new request). Never
|
|
73
|
+
reshape or re-sort the rest of the response.
|
|
74
|
+
|
|
75
|
+
## Wire a company pick into the listing
|
|
76
|
+
|
|
77
|
+
Company slugs are the URL identity. Map them onto the jobs surface as
|
|
78
|
+
`companySlug` — never resolve slug→id client-side:
|
|
79
|
+
|
|
80
|
+
```ts snippet
|
|
81
|
+
import { parseListingFilters } from '@cavuno/board/filters';
|
|
82
|
+
|
|
83
|
+
const filters = parseListingFilters(rawSearchParams);
|
|
84
|
+
// filters.company is string[] of public slugs
|
|
85
|
+
|
|
86
|
+
const page = await board.jobs.list({
|
|
87
|
+
limit: 20,
|
|
88
|
+
companySlug: filters.company,
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Unknown slugs are dropped server-side (not an error). If the entire company
|
|
93
|
+
filter resolves to no known companies, the listing is empty (`count: 0`).
|
|
94
|
+
Combined with `companyId` as a union when both are set.
|
|
95
|
+
|
|
96
|
+
## Anti-patterns
|
|
97
|
+
|
|
98
|
+
```ts no-check
|
|
99
|
+
// NEVER re-sort server-ranked items:
|
|
100
|
+
items.sort((a, b) => a.name.localeCompare(b.name));
|
|
101
|
+
// NEVER resolve company slug → id on the client:
|
|
102
|
+
const id = await lookupCompanyId(item.slug);
|
|
103
|
+
board.jobs.list({ companyId: [id] });
|
|
104
|
+
// NEVER hand-roll debounce/abort — use the controller so patches ship via npm.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Out of scope — do not invent exports
|
|
108
|
+
|
|
109
|
+
No React components, no dropdown UI, no keyboard navigation — this package
|
|
110
|
+
is the data + controller layer. Location suggest stays on `taxonomy.places`.
|
|
111
|
+
|
|
112
|
+
## Verify
|
|
113
|
+
|
|
114
|
+
- [ ] Typing under 2 characters shows no suggestions and no network call
|
|
115
|
+
from the controller.
|
|
116
|
+
- [ ] Fast typing cancels the previous request; only the final query's
|
|
117
|
+
results render.
|
|
118
|
+
- [ ] Selecting a company writes its public slug into the URL and the
|
|
119
|
+
listing request uses `companySlug`, not a client-resolved id.
|
package/skills/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.40.0",
|
|
3
3
|
"skills": [
|
|
4
4
|
{
|
|
5
5
|
"name": "cavuno-board-account",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
"name": "cavuno-board-filters",
|
|
55
|
-
"description": "The canonical listing-filter vocabulary and URL search-param parsing with @cavuno/board/filters — remote/employment/seniority/sort sets, localized seniority labels, and parseListingFilters for jobs-listing routes. Use when building listing pages, filter sidebars, sort dropdowns, or validating listing URL params.",
|
|
55
|
+
"description": "The canonical listing-filter vocabulary and URL search-param parsing with @cavuno/board/filters — remote/employment/seniority/company/sort sets, localized seniority labels, and parseListingFilters for jobs-listing routes. Use when building listing pages, filter sidebars, sort dropdowns, or validating listing URL params.",
|
|
56
56
|
"path": "skills/cavuno-board-filters/SKILL.md",
|
|
57
57
|
"framework": null,
|
|
58
58
|
"category": "core"
|
|
@@ -148,6 +148,13 @@
|
|
|
148
148
|
"framework": null,
|
|
149
149
|
"category": "core"
|
|
150
150
|
},
|
|
151
|
+
{
|
|
152
|
+
"name": "cavuno-board-suggest",
|
|
153
|
+
"description": "Federated keyword search suggestions with the @cavuno/board SDK — board.search.suggest for companies + taxonomy terms in one server-ranked list, and the headless createSuggestController under @cavuno/board/suggest (debounce, abort, stale-drop). Use when building a search dropdown, typeahead, or autocomplete that mirrors the hosted board.",
|
|
154
|
+
"path": "skills/cavuno-board-suggest/SKILL.md",
|
|
155
|
+
"framework": null,
|
|
156
|
+
"category": "core"
|
|
157
|
+
},
|
|
151
158
|
{
|
|
152
159
|
"name": "cavuno-board-tanstack-start",
|
|
153
160
|
"description": "TanStack-Start-on-Cloudflare-Workers reference wiring for a headless Cavuno board — SSR loaders calling @cavuno/board server-side, the session held in an __Host- httpOnly cookie owned by the app, a single-flight refresh helper, and FetchOptions cache passthrough on Workers.",
|