jamdesk 1.1.212 → 1.1.214
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/unit/docs-config.test.js +26 -0
- package/dist/__tests__/unit/docs-config.test.js.map +1 -1
- package/dist/__tests__/unit/dusk-embargo.test.d.ts +2 -0
- package/dist/__tests__/unit/dusk-embargo.test.d.ts.map +1 -0
- package/dist/__tests__/unit/dusk-embargo.test.js +25 -0
- package/dist/__tests__/unit/dusk-embargo.test.js.map +1 -0
- package/dist/__tests__/unit/migrate-resolve-theme.test.js +11 -0
- package/dist/__tests__/unit/migrate-resolve-theme.test.js.map +1 -1
- package/dist/commands/migrate/index.d.ts.map +1 -1
- package/dist/commands/migrate/index.js +7 -3
- package/dist/commands/migrate/index.js.map +1 -1
- package/dist/commands/migrate/types.d.ts +4 -1
- package/dist/commands/migrate/types.d.ts.map +1 -1
- package/dist/commands/migrate/types.js +13 -1
- package/dist/commands/migrate/types.js.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/deps.js +1 -1
- package/dist/lib/docs-config.d.ts +1 -1
- package/dist/lib/docs-config.d.ts.map +1 -1
- package/dist/lib/docs-config.js +1 -1
- package/dist/lib/docs-config.js.map +1 -1
- package/dist/lib/dusk-embargo.d.ts +15 -0
- package/dist/lib/dusk-embargo.d.ts.map +1 -0
- package/dist/lib/dusk-embargo.js +15 -0
- package/dist/lib/dusk-embargo.js.map +1 -0
- package/dist/lib/normalize-config.js +1 -1
- package/dist/lib/normalize-config.js.map +1 -1
- package/package.json +3 -3
- package/vendored/app/(unlock)/jd/unlock/page.tsx +2 -2
- package/vendored/app/api/markdown-export/[project]/[...slug]/route.ts +31 -1
- package/vendored/components/mdx/Tooltip.tsx +27 -1
- package/vendored/components/navigation/DefaultLogo.tsx +25 -18
- package/vendored/components/navigation/Header.tsx +78 -29
- package/vendored/components/navigation/Sidebar.tsx +5 -3
- package/vendored/components/navigation/SocialFooter.tsx +5 -6
- package/vendored/components/navigation/ThemePreviewChips.tsx +3 -2
- package/vendored/lib/branding-url.ts +9 -0
- package/vendored/lib/build/error-parser.ts +1 -1
- package/vendored/lib/docs-types.ts +5 -1
- package/vendored/lib/dusk-embargo.ts +16 -0
- package/vendored/lib/email-notifier.ts +6 -8
- package/vendored/lib/extract-highlights.ts +1 -1
- package/vendored/lib/layout-helpers.tsx +19 -2
- package/vendored/lib/mailfully.ts +86 -0
- package/vendored/lib/markdown-absolute-links.ts +197 -0
- package/vendored/lib/middleware-helpers.ts +24 -0
- package/vendored/lib/normalize-config.ts +1 -1
- package/vendored/lib/r2-cleanup.ts +14 -10
- package/vendored/lib/screenshot-capture.ts +88 -16
- package/vendored/lib/static-artifacts.ts +92 -31
- package/vendored/lib/static-file-route.ts +25 -8
- package/vendored/lib/theme-preview.ts +16 -8
- package/vendored/lib/validate-config.ts +4 -4
- package/vendored/schema/docs-schema.json +124 -0
- package/vendored/themes/dusk/variables.css +476 -0
- package/vendored/themes/index.ts +28 -0
- package/vendored/themes/types.ts +8 -0
- package/vendored/workspace-package-lock.json +187 -164
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dusk launch embargo — flip to `true` on October 1, 2026.
|
|
3
|
+
*
|
|
4
|
+
* One of four twins (build-service, CLI, dashboard, marketing); separate apps
|
|
5
|
+
* with separate deploys, so they cannot share a module. Flip all four in one
|
|
6
|
+
* commit. See docs/plans/2026-09-23-dusk-october-1-release-plan.md.
|
|
7
|
+
*
|
|
8
|
+
* Here it gates the `jamdesk migrate` theme prompt and the `--theme` help.
|
|
9
|
+
* Validation is NOT gated.
|
|
10
|
+
*
|
|
11
|
+
* The `: boolean` annotation is LOAD-BEARING: without it TS narrows to the
|
|
12
|
+
* literal `false` and stops type-checking the launch branch.
|
|
13
|
+
*/
|
|
14
|
+
export const DUSK_PUBLIC = false;
|
|
15
|
+
//# sourceMappingURL=dusk-embargo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dusk-embargo.js","sourceRoot":"","sources":["../../src/lib/dusk-embargo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,WAAW,GAAY,KAAK,CAAC"}
|
|
@@ -62,7 +62,7 @@ export function normalizeConfig(config) {
|
|
|
62
62
|
}
|
|
63
63
|
// 3. Warn about layout
|
|
64
64
|
if (layout) {
|
|
65
|
-
warnings.push('layout field is ignored. Jamdesk determines layout from theme (pulsar=sidebar, jam/nebula/halo=header).');
|
|
65
|
+
warnings.push('layout field is ignored. Jamdesk determines layout from theme (pulsar=sidebar, jam/nebula/halo/dusk=header).');
|
|
66
66
|
}
|
|
67
67
|
// 4. Warn about navbar.style
|
|
68
68
|
if (rest.navbar?.style) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize-config.js","sourceRoot":"","sources":["../../src/lib/normalize-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAmCH;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,MAAmB;IACjD,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC;IACzD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,MAAM,UAAU,GAAgB,EAAE,GAAG,IAAI,EAAE,CAAC;IAE5C,wCAAwC;IACxC,IAAI,UAAU,EAAE,CAAC;QACf,QAAQ,CAAC,IAAI,CACX,sFAAsF,CACvF,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACvB,UAAU,CAAC,UAAU,GAAG;gBACtB,OAAO,EAAE,UAAU,CAAC,OAAO,IAAI,QAAQ;gBACvC,MAAM,EAAE,UAAU,CAAC,QAAQ,KAAK,IAAI;aACrC,CAAC;QACJ,CAAC;IACH,CAAC;IAED,yBAAyB;IACzB,sFAAsF;IACtF,iFAAiF;IACjF,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,EAAE,SAAS,EAAE,GAAG,cAAc,EAAE,GAAG,QAAQ,CAAC;QAElD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,UAAU,CAAC,QAAQ,GAAG,EAAE,SAAS,EAAE,SAAS,KAAK,IAAI,EAAE,CAAC;QAC1D,CAAC;QAED,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC/C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,QAAQ,CAAC,IAAI,CACX,iEAAiE,CAClE,CAAC;YACF,MAAM,gBAAgB,GAAI,UAAU,CAAC,GAAW,EAAE,QAAQ,IAAI,EAAE,CAAC;YACjE,UAAU,CAAC,GAAG,GAAG;gBACf,GAAG,UAAU,CAAC,GAAG;gBACjB,QAAQ,EAAE;oBACR,GAAI,cAAyC;oBAC7C,GAAG,gBAAgB;iBACpB;aACF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,uBAAuB;IACvB,IAAI,MAAM,EAAE,CAAC;QACX,QAAQ,CAAC,IAAI,CACX,
|
|
1
|
+
{"version":3,"file":"normalize-config.js","sourceRoot":"","sources":["../../src/lib/normalize-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAmCH;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,MAAmB;IACjD,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC;IACzD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,MAAM,UAAU,GAAgB,EAAE,GAAG,IAAI,EAAE,CAAC;IAE5C,wCAAwC;IACxC,IAAI,UAAU,EAAE,CAAC;QACf,QAAQ,CAAC,IAAI,CACX,sFAAsF,CACvF,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACvB,UAAU,CAAC,UAAU,GAAG;gBACtB,OAAO,EAAE,UAAU,CAAC,OAAO,IAAI,QAAQ;gBACvC,MAAM,EAAE,UAAU,CAAC,QAAQ,KAAK,IAAI;aACrC,CAAC;QACJ,CAAC;IACH,CAAC;IAED,yBAAyB;IACzB,sFAAsF;IACtF,iFAAiF;IACjF,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,EAAE,SAAS,EAAE,GAAG,cAAc,EAAE,GAAG,QAAQ,CAAC;QAElD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,UAAU,CAAC,QAAQ,GAAG,EAAE,SAAS,EAAE,SAAS,KAAK,IAAI,EAAE,CAAC;QAC1D,CAAC;QAED,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC/C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,QAAQ,CAAC,IAAI,CACX,iEAAiE,CAClE,CAAC;YACF,MAAM,gBAAgB,GAAI,UAAU,CAAC,GAAW,EAAE,QAAQ,IAAI,EAAE,CAAC;YACjE,UAAU,CAAC,GAAG,GAAG;gBACf,GAAG,UAAU,CAAC,GAAG;gBACjB,QAAQ,EAAE;oBACR,GAAI,cAAyC;oBAC7C,GAAG,gBAAgB;iBACpB;aACF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,uBAAuB;IACvB,IAAI,MAAM,EAAE,CAAC;QACX,QAAQ,CAAC,IAAI,CACX,8GAA8G,CAC/G,CAAC;IACJ,CAAC;IAED,6BAA6B;IAC7B,IAAI,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;QACvB,QAAQ,CAAC,IAAI,CACX,6DAA6D,CAC9D,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;AAC1C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.214",
|
|
4
4
|
"description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jamdesk",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"@inquirer/prompts": "^8.7.2",
|
|
107
107
|
"ajv": "^8.17.1",
|
|
108
108
|
"ajv-formats": "^3.0.1",
|
|
109
|
-
"chalk": "^6.0.
|
|
109
|
+
"chalk": "^6.0.1",
|
|
110
110
|
"commander": "^15.0.0",
|
|
111
111
|
"dictionary-en": "^4.0.0",
|
|
112
112
|
"estree-util-visit": "^2.0.0",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
"devDependencies": {
|
|
130
130
|
"@mdx-js/mdx": "^3.1.1",
|
|
131
131
|
"@types/fs-extra": "^11.0.0",
|
|
132
|
-
"@types/node": "^26.
|
|
132
|
+
"@types/node": "^26.6.3",
|
|
133
133
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
134
134
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
135
135
|
"vitest": "^4.1.11"
|
|
@@ -2,7 +2,7 @@ import { headers } from 'next/headers';
|
|
|
2
2
|
import { getDocsConfig } from '@/lib/docs-isr';
|
|
3
3
|
import { normalizeLogo } from '@/lib/docs-types';
|
|
4
4
|
import { sanitizeFrom } from '@/lib/sanitize-from';
|
|
5
|
-
import { getBrandingUrl } from '@/lib/branding-url';
|
|
5
|
+
import { getBrandingUrl, isBrandingVisible } from '@/lib/branding-url';
|
|
6
6
|
import UnlockForm from './UnlockForm';
|
|
7
7
|
|
|
8
8
|
interface UnlockPageProps {
|
|
@@ -34,7 +34,7 @@ export default async function UnlockPage({ searchParams }: UnlockPageProps) {
|
|
|
34
34
|
const from = sanitizeFrom(rawFrom ?? null);
|
|
35
35
|
const showError = params.error === '1';
|
|
36
36
|
|
|
37
|
-
const showBranding =
|
|
37
|
+
const showBranding = isBrandingVisible(config);
|
|
38
38
|
const brandingUrl = getBrandingUrl(slug);
|
|
39
39
|
|
|
40
40
|
return (
|
|
@@ -20,6 +20,7 @@ import { apiSpecsMarkdownFooter } from '@/lib/api-specs-markdown-hint';
|
|
|
20
20
|
import { collectLocalSpecPaths } from '@/lib/openapi/operation-description';
|
|
21
21
|
import { operationMarkdownForRef } from '@/lib/openapi/enrich-page-infos';
|
|
22
22
|
import { injectAgentDirective } from '@/lib/agent-directive';
|
|
23
|
+
import { absolutizeMarkdownLinks } from '@/lib/markdown-absolute-links';
|
|
23
24
|
import { resolveMarkdownFallbackPath } from '@/lib/root-page-slug';
|
|
24
25
|
import { logger } from '@/shared/logger';
|
|
25
26
|
import type { DocsConfig } from '@/lib/docs-types';
|
|
@@ -153,7 +154,9 @@ export async function GET(
|
|
|
153
154
|
// path returns whatever the source MDX happened to end with. 107 of snag's
|
|
154
155
|
// 115 exports shipped without it, so anything appending to or concatenating
|
|
155
156
|
// these files ran the last line of one into the next.
|
|
156
|
-
|
|
157
|
+
// Last, so links in the appended OpenAPI operation content are covered too.
|
|
158
|
+
const linked = absolutizeLinks(hinted, request.headers, project, hostAtDocs, linkPrefix);
|
|
159
|
+
const body = `${linked.replace(/\s+$/, '')}\n`;
|
|
157
160
|
|
|
158
161
|
// Two paths reach this handler via proxy rewrite:
|
|
159
162
|
// (a) `.md` URL: unique CDN cache key, safe to share.
|
|
@@ -177,6 +180,33 @@ export async function GET(
|
|
|
177
180
|
});
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Root-relative links in the export become absolute URLs on the host the
|
|
185
|
+
* reader asked for (see absolutizeMarkdownLinks for why). ISR only: `jamdesk
|
|
186
|
+
* dev` serves from localhost over http, and getBaseUrl always builds https.
|
|
187
|
+
* The base comes from getBaseUrl, the same trusted-header resolution the
|
|
188
|
+
* api-specs footer and canonical tags use, so a custom domain's own prefix
|
|
189
|
+
* wins over the subdomain's. Never throws.
|
|
190
|
+
*/
|
|
191
|
+
function absolutizeLinks(
|
|
192
|
+
markdown: string,
|
|
193
|
+
headers: Headers,
|
|
194
|
+
project: string,
|
|
195
|
+
hostAtDocs: boolean,
|
|
196
|
+
linkPrefix: string,
|
|
197
|
+
): string {
|
|
198
|
+
if (!isIsrMode()) return markdown;
|
|
199
|
+
try {
|
|
200
|
+
return absolutizeMarkdownLinks(markdown, getBaseUrl(headers, project, hostAtDocs), linkPrefix);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
logger.warn('[markdown-export] link absolutize skipped', {
|
|
203
|
+
project,
|
|
204
|
+
error: err instanceof Error ? err.message : String(err),
|
|
205
|
+
});
|
|
206
|
+
return markdown;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
180
210
|
/**
|
|
181
211
|
* The two shapes fetchMdxContent throws for a missing page — the
|
|
182
212
|
* `Page not found: …` message the S3 path builds and the error names the
|
|
@@ -26,6 +26,7 @@ export function Tooltip({
|
|
|
26
26
|
const linkPrefix = useLinkPrefix();
|
|
27
27
|
const [isVisible, setIsVisible] = useState(false);
|
|
28
28
|
const [position, setPosition] = useState<'top' | 'bottom'>('top');
|
|
29
|
+
const [shiftX, setShiftX] = useState(0);
|
|
29
30
|
const triggerRef = useRef<HTMLSpanElement>(null);
|
|
30
31
|
const tooltipRef = useRef<HTMLSpanElement>(null);
|
|
31
32
|
const tooltipId = useId();
|
|
@@ -39,6 +40,19 @@ export function Tooltip({
|
|
|
39
40
|
|
|
40
41
|
// Show below if not enough space above
|
|
41
42
|
setPosition(spaceAbove < tooltipHeight + 8 ? 'bottom' : 'top');
|
|
43
|
+
|
|
44
|
+
// The popover is centered on the trigger, so a trigger near the edge of
|
|
45
|
+
// the content column pushes half of it past the column's overflow edge,
|
|
46
|
+
// where it is clipped. Shift it back inside the nearest clipping
|
|
47
|
+
// ancestor (the viewport if there is none); the arrow counter-shifts so
|
|
48
|
+
// it still points at the trigger.
|
|
49
|
+
const bounds = clippingBounds(triggerRef.current);
|
|
50
|
+
const center = triggerRect.left + triggerRect.width / 2;
|
|
51
|
+
const halfWidth = tooltipRef.current.offsetWidth / 2;
|
|
52
|
+
const margin = 8;
|
|
53
|
+
const overflowLeft = bounds.left + margin - (center - halfWidth);
|
|
54
|
+
const overflowRight = center + halfWidth - (bounds.right - margin);
|
|
55
|
+
setShiftX(overflowLeft > 0 ? overflowLeft : overflowRight > 0 ? -overflowRight : 0);
|
|
42
56
|
}
|
|
43
57
|
}, [isVisible]);
|
|
44
58
|
|
|
@@ -64,7 +78,7 @@ export function Tooltip({
|
|
|
64
78
|
ref={tooltipRef}
|
|
65
79
|
id={tooltipId}
|
|
66
80
|
role="tooltip"
|
|
67
|
-
style={{ boxShadow: 'var(--shadow-lg)', display: 'block' }}
|
|
81
|
+
style={{ boxShadow: 'var(--shadow-lg)', display: 'block', marginLeft: `${shiftX}px` }}
|
|
68
82
|
className={`absolute z-50 w-64 p-3 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] transition-all duration-150 ${
|
|
69
83
|
position === 'top'
|
|
70
84
|
? 'bottom-full left-1/2 -translate-x-1/2 mb-2'
|
|
@@ -99,6 +113,7 @@ export function Tooltip({
|
|
|
99
113
|
|
|
100
114
|
{/* Arrow */}
|
|
101
115
|
<span
|
|
116
|
+
style={{ marginLeft: `${-shiftX}px` }}
|
|
102
117
|
className={`absolute left-1/2 -translate-x-1/2 w-2 h-2 rotate-45 border-[var(--color-border)] bg-[var(--color-bg-primary)] ${
|
|
103
118
|
position === 'top'
|
|
104
119
|
? 'bottom-[-5px] border-r border-b'
|
|
@@ -109,3 +124,14 @@ export function Tooltip({
|
|
|
109
124
|
</span>
|
|
110
125
|
);
|
|
111
126
|
}
|
|
127
|
+
|
|
128
|
+
/** Left/right edges of the nearest ancestor that clips horizontally. */
|
|
129
|
+
function clippingBounds(el: HTMLElement): { left: number; right: number } {
|
|
130
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
131
|
+
if (getComputedStyle(node).overflowX !== 'visible') {
|
|
132
|
+
const rect = node.getBoundingClientRect();
|
|
133
|
+
return { left: rect.left, right: rect.right };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { left: 0, right: document.documentElement.clientWidth };
|
|
137
|
+
}
|
|
@@ -12,27 +12,34 @@ interface DefaultLogoProps {
|
|
|
12
12
|
|
|
13
13
|
export function DefaultLogo({ name, showBadge = true }: DefaultLogoProps) {
|
|
14
14
|
return (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
15
|
+
// In a crowded header the badge goes first, then the name cuts off with
|
|
16
|
+
// "…", rather than either pushing the controls beside it off-screen. The
|
|
17
|
+
// badge goes by wrapping: this row is one line tall, clipped and allowed to
|
|
18
|
+
// wrap, so the badge drops to the hidden second line as soon as icon +
|
|
19
|
+
// name + badge stop fitting.
|
|
20
|
+
<div className="flex flex-wrap content-start items-center gap-x-2.5 h-8 overflow-hidden min-w-0 group">
|
|
21
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
22
|
+
{/* Jamdesk icon: Three overlapping rounded rectangles */}
|
|
23
|
+
<div className="relative w-8 h-8 flex-shrink-0 transition-transform duration-200 group-hover:scale-105">
|
|
24
|
+
<svg viewBox="0 0 32 32" fill="none" className="w-8 h-8">
|
|
25
|
+
{/* Back rectangle (left) - lightest purple */}
|
|
26
|
+
<rect x="3" y="7" width="14" height="19" rx="2.5" fill="#C4B5FD" fillOpacity="0.9"/>
|
|
27
|
+
{/* Middle rectangle - medium purple */}
|
|
28
|
+
<rect x="8" y="4" width="14" height="19" rx="2.5" fill="#A78BFA" fillOpacity="0.85"/>
|
|
29
|
+
{/* Front rectangle (right) - darker purple */}
|
|
30
|
+
<rect x="13" y="9" width="14" height="19" rx="2.5" fill="#8B5CF6" fillOpacity="0.9"/>
|
|
31
|
+
</svg>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{/* Site name */}
|
|
35
|
+
<span className="text-lg font-semibold text-[var(--color-text-primary)] tracking-tight truncate">
|
|
36
|
+
{name}
|
|
37
|
+
</span>
|
|
26
38
|
</div>
|
|
27
|
-
|
|
28
|
-
{/* Site name */}
|
|
29
|
-
<span className="text-lg font-semibold text-[var(--color-text-primary)] tracking-tight">
|
|
30
|
-
{name}
|
|
31
|
-
</span>
|
|
32
|
-
|
|
39
|
+
|
|
33
40
|
{/* Docs badge */}
|
|
34
41
|
{showBadge && (
|
|
35
|
-
<span className="px-2 py-0.5 text-[10px] font-bold bg-[var(--color-accent)]/10 text-[var(--color-accent)] rounded uppercase tracking-wide border border-[var(--color-accent)]/20">
|
|
42
|
+
<span className="flex-shrink-0 px-2 py-0.5 text-[10px] font-bold bg-[var(--color-accent)]/10 text-[var(--color-accent)] rounded uppercase tracking-wide border border-[var(--color-accent)]/20">
|
|
36
43
|
docs
|
|
37
44
|
</span>
|
|
38
45
|
)}
|
|
@@ -9,7 +9,7 @@ import type { DocsConfig, TabsPosition } from '@/lib/docs-types';
|
|
|
9
9
|
import { normalizeLogo, getIconString } from '@/lib/docs-types';
|
|
10
10
|
import type { LayoutVariant } from '@/themes';
|
|
11
11
|
import { getTheme } from '@/themes';
|
|
12
|
-
import { ThemeToggle } from '@/components/theme/ThemeToggle';
|
|
12
|
+
import { ThemeToggle, ThemeToggleCycle } from '@/components/theme/ThemeToggle';
|
|
13
13
|
import { LazySearchModal as SearchModal } from '@/components/search/LazySearchModal';
|
|
14
14
|
import { DefaultLogo } from './DefaultLogo';
|
|
15
15
|
import { LanguageSelector } from './LanguageSelector';
|
|
@@ -33,11 +33,29 @@ interface HeaderProps {
|
|
|
33
33
|
// Max tabs to show before overflow dropdown
|
|
34
34
|
const MAX_VISIBLE_TABS = 4;
|
|
35
35
|
|
|
36
|
-
// Shared button classes for
|
|
36
|
+
// Shared button classes for search/AI icons below lg. Every search/AI control
|
|
37
|
+
// shows at exactly one set of widths: these below lg, the full bar or the
|
|
38
|
+
// compact icons from lg (header-responsive.test.tsx).
|
|
39
|
+
//
|
|
40
|
+
// Viewport breakpoints decide what lives in the header versus the drawer,
|
|
41
|
+
// because the drawer only exists below lg. How much room a control takes keys
|
|
42
|
+
// off container widths instead: from xl the inline chat panel takes 350-500px
|
|
43
|
+
// of the viewport, and with viewport breakpoints alone Jam's row ran 120px
|
|
44
|
+
// past its edge at 1280 with chat open.
|
|
37
45
|
const MOBILE_ICON_BUTTON = 'lg:hidden p-2 text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] bg-[var(--color-bg-secondary)]/50 border border-[var(--color-border)]/50 hover:bg-[var(--color-bg-secondary)] rounded-md transition-colors cursor-pointer';
|
|
38
46
|
|
|
47
|
+
// Full search bar buttons (Jam, Halo): icon | label | ⌘ hint. Grid rather
|
|
48
|
+
// than flex so the label column's minimum is 0: as a flex item a nowrap label
|
|
49
|
+
// holds its full width as the bar's minimum and overflows instead of cutting
|
|
50
|
+
// off. Margins rather than gap, so a hidden label or hint leaves no stray gap.
|
|
51
|
+
// The fixed height keeps an icon-only button as tall as a labelled one (the
|
|
52
|
+
// label's text-sm line sets it; Font Awesome's own line-height beats leading-*).
|
|
53
|
+
const BAR_BUTTON = 'grid grid-cols-[auto_minmax(0,max-content)_auto] items-center h-9.5 px-3 py-2 bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded-lg';
|
|
54
|
+
const BAR_LABEL = 'col-start-2 ml-3 hidden @min-[16rem]:block truncate';
|
|
55
|
+
const BAR_HINT = 'col-start-3 ml-3 hidden @min-[24rem]:inline-flex';
|
|
56
|
+
|
|
39
57
|
// Shared button classes for compact (Nebula) header icon buttons
|
|
40
|
-
const COMPACT_ICON_BUTTON = 'hidden
|
|
58
|
+
const COMPACT_ICON_BUTTON = 'hidden lg:flex p-2 hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] rounded-lg transition-colors cursor-pointer';
|
|
41
59
|
|
|
42
60
|
export function Header({ config, layout = 'header-logo', tabsPosition: tabsPositionProp, isSidebarOpen, onToggleSidebar }: HeaderProps) {
|
|
43
61
|
const { onChatOpen, devNotice } = useChatOpenHandler();
|
|
@@ -257,12 +275,14 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
257
275
|
const renderLogo = () => {
|
|
258
276
|
if (showLogoImage) {
|
|
259
277
|
return (
|
|
260
|
-
<div className="flex items-center gap-2">
|
|
278
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
261
279
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
262
280
|
<img
|
|
263
281
|
src={logoSrc}
|
|
264
282
|
alt={`${config.name} logo`}
|
|
265
|
-
|
|
283
|
+
// minWidth 0 lets the logo give way when the row runs out of room;
|
|
284
|
+
// contain keeps it from squashing while it does.
|
|
285
|
+
style={{ height: '28px', width: 'auto', maxWidth: '170px', minWidth: 0, objectFit: 'contain', objectPosition: 'left center' }}
|
|
266
286
|
onError={() => setLogoError(true)}
|
|
267
287
|
/>
|
|
268
288
|
</div>
|
|
@@ -291,9 +311,12 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
291
311
|
CSS that wants to detect the canonical tab strip's presence should target
|
|
292
312
|
`.sidebar-scroll[data-has-tabs="true"]` instead (set by Sidebar.tsx). */}
|
|
293
313
|
<header className={headerClasses} data-has-tabs={showTabsInHeader && hasTabs ? 'true' : 'false'}>
|
|
294
|
-
<div className="max-w-[1440px] 2xl:mx-auto flex items-center py-3" style={{ margin: '0 24px' }}>
|
|
295
|
-
{/* Left: Hamburger + Logo
|
|
296
|
-
|
|
314
|
+
<div className="@container max-w-[1440px] 2xl:mx-auto flex items-center py-3" style={{ margin: '0 24px' }}>
|
|
315
|
+
{/* Left: Hamburger + Logo. The one block that can shrink: when the
|
|
316
|
+
row is still too narrow, the logo gives way (a text logo cuts off
|
|
317
|
+
with "…") instead of pushing Ask AI or the theme toggle off-screen.
|
|
318
|
+
A logo-less site's name + badge overran 375px by 13-20px. */}
|
|
319
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
297
320
|
{/* Hamburger menu - mobile only */}
|
|
298
321
|
<button
|
|
299
322
|
onClick={onToggleSidebar}
|
|
@@ -309,11 +332,11 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
309
332
|
|
|
310
333
|
{/* Logo - always shown in header-logo layout, shown on mobile only for sidebar-logo layout */}
|
|
311
334
|
{showLogoInHeader ? (
|
|
312
|
-
<Link href={logoHref} prefetch={false} className="flex items-center">
|
|
335
|
+
<Link href={logoHref} prefetch={false} className="flex items-center min-w-0">
|
|
313
336
|
{renderLogo()}
|
|
314
337
|
</Link>
|
|
315
338
|
) : (
|
|
316
|
-
<Link href={logoHref} prefetch={false} className="lg:hidden flex items-center">
|
|
339
|
+
<Link href={logoHref} prefetch={false} className="lg:hidden flex items-center min-w-0">
|
|
317
340
|
{renderLogo()}
|
|
318
341
|
</Link>
|
|
319
342
|
)}
|
|
@@ -321,16 +344,30 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
321
344
|
|
|
322
345
|
{/* Center: Search Bar + Ask AI Button - only in header-logo layout, not for compact search themes */}
|
|
323
346
|
{showLogoInHeader && !useCompactSearch && (
|
|
324
|
-
|
|
325
|
-
|
|
347
|
+
// From lg only: below it the icon buttons cover search/AI, and the
|
|
348
|
+
// column is just a spacer, so it carries no padding either.
|
|
349
|
+
//
|
|
350
|
+
// The column is the container the labels and ⌘ hints query: its
|
|
351
|
+
// width is exactly the room left between the logo and the right
|
|
352
|
+
// side, whatever took the rest (the chat panel, a long navbar, a
|
|
353
|
+
// wide logo). Labels need 16rem, which covers English with room to
|
|
354
|
+
// spare and French or Portuguese with a little cut off ("…", never
|
|
355
|
+
// wrapped: "Demander à l'IA" wrapped to a 57px-tall button at
|
|
356
|
+
// 1024). The hints need 24rem. Size containment means the column
|
|
357
|
+
// no longer takes a minimum from its content, so lg:min-w-[8rem]
|
|
358
|
+
// holds the two icon-only buttons (2 x 43px + gap + padding); past
|
|
359
|
+
// that the logo gives way.
|
|
360
|
+
<div className="@container flex-1 flex justify-center lg:px-4 lg:min-w-[8rem]">
|
|
361
|
+
<div className="hidden lg:flex items-center gap-2">
|
|
326
362
|
<button
|
|
327
363
|
onClick={() => onSearchOpen()}
|
|
328
|
-
className=
|
|
364
|
+
className={`${BAR_BUTTON} text-[var(--color-text-muted)] text-sm hover:border-[var(--color-border-hover)] transition-colors cursor-pointer`}
|
|
329
365
|
aria-label={ui.search}
|
|
366
|
+
title={`${ui.search} (⌘K)`}
|
|
330
367
|
>
|
|
331
|
-
<i className="fa-solid fa-magnifying-glass text-[14px]
|
|
332
|
-
<span>{ui.search}</span>
|
|
333
|
-
<kbd className=
|
|
368
|
+
<i className="fa-solid fa-magnifying-glass text-[14px]" aria-hidden="true" />
|
|
369
|
+
<span className={BAR_LABEL}>{ui.search}</span>
|
|
370
|
+
<kbd className={`${BAR_HINT} items-center gap-[2px] px-2 py-0.5 text-xs text-[var(--color-text-muted)] bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded`}>
|
|
334
371
|
<span className="text-sm leading-none">⌘</span>
|
|
335
372
|
<span>K</span>
|
|
336
373
|
</kbd>
|
|
@@ -338,13 +375,13 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
338
375
|
{onChatOpen && (
|
|
339
376
|
<button
|
|
340
377
|
onClick={onChatOpen}
|
|
341
|
-
className=
|
|
378
|
+
className={`${BAR_BUTTON} hover:border-[var(--color-border-hover)] cursor-pointer transition-colors text-sm text-[var(--color-text-muted)]`}
|
|
342
379
|
aria-label={ui.askAi}
|
|
343
380
|
title={`${ui.askAi} (⌘I)`}
|
|
344
381
|
>
|
|
345
382
|
<i className="fa-solid fa-sparkles text-[14px] text-[var(--color-accent)]" aria-hidden="true" />
|
|
346
|
-
<span>{ui.askAi}</span>
|
|
347
|
-
<kbd className=
|
|
383
|
+
<span className={BAR_LABEL}>{ui.askAi}</span>
|
|
384
|
+
<kbd className={`${BAR_HINT} items-center gap-[2px] px-2 py-0.5 text-xs text-[var(--color-text-muted)] bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded`}>
|
|
348
385
|
<span className="text-sm leading-none">⌘</span>
|
|
349
386
|
<span>I</span>
|
|
350
387
|
</kbd>
|
|
@@ -517,14 +554,18 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
517
554
|
</a>
|
|
518
555
|
)}
|
|
519
556
|
|
|
520
|
-
{/* Language Selector -
|
|
557
|
+
{/* Language Selector - header-logo layout, desktop only. Below lg it
|
|
558
|
+
lives in the sidebar drawer, like the theme toggle: at 375px the
|
|
559
|
+
row can't fit it beside the logo. */}
|
|
521
560
|
{showLogoInHeader && resolved?.languages && resolved.languages.length > 1 && (
|
|
522
|
-
<
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
561
|
+
<div className="hidden lg:block">
|
|
562
|
+
<LanguageSelector
|
|
563
|
+
languages={resolved.languages}
|
|
564
|
+
compact={true}
|
|
565
|
+
position="down"
|
|
566
|
+
defaultLanguage={resolved.languages.find((l) => l.isDefault)?.code}
|
|
567
|
+
/>
|
|
568
|
+
</div>
|
|
528
569
|
)}
|
|
529
570
|
|
|
530
571
|
{/* Compact search + AI icons - for themes like Nebula that use icon-only buttons */}
|
|
@@ -556,10 +597,18 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
556
597
|
{config.auth?.jwt?.enabled === true && <LogoutButton />}
|
|
557
598
|
|
|
558
599
|
{/* Theme toggle - hidden on mobile since it's in the sidebar menu.
|
|
559
|
-
Also hidden when appearance.strict pins users to the configured mode.
|
|
560
|
-
|
|
600
|
+
Also hidden when appearance.strict pins users to the configured mode.
|
|
601
|
+
Header-logo only: sidebar-logo (Pulsar) keeps it in the sidebar.
|
|
602
|
+
In a row under 48rem (the chat panel dragged to 500px at 1280)
|
|
603
|
+
the three-button toggle gives way to the one-button cycle. */}
|
|
604
|
+
{showLogoInHeader && !config.appearance?.strict && (
|
|
561
605
|
<div className="hidden lg:block">
|
|
562
|
-
<
|
|
606
|
+
<div className="hidden @min-[48rem]:block">
|
|
607
|
+
<ThemeToggle />
|
|
608
|
+
</div>
|
|
609
|
+
<div className="flex @min-[48rem]:hidden">
|
|
610
|
+
<ThemeToggleCycle />
|
|
611
|
+
</div>
|
|
563
612
|
</div>
|
|
564
613
|
)}
|
|
565
614
|
</div>
|
|
@@ -964,9 +964,11 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
964
964
|
</div>
|
|
965
965
|
</nav>
|
|
966
966
|
|
|
967
|
-
{/* Language Selector -
|
|
968
|
-
|
|
969
|
-
|
|
967
|
+
{/* Language Selector. Sidebar-logo (Pulsar) keeps it here at every
|
|
968
|
+
width. Header-logo layouts show it in the header from lg and here,
|
|
969
|
+
in the drawer, below it (header-responsive.test.tsx). */}
|
|
970
|
+
{resolvedNav.languages.length > 1 && (
|
|
971
|
+
<div className={`px-4 py-3 border-t border-[var(--color-border)]${layout === 'sidebar-logo' ? '' : ' lg:hidden'}`}>
|
|
970
972
|
<LanguageSelector
|
|
971
973
|
languages={resolvedNav.languages}
|
|
972
974
|
position="up"
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
import type { CSSProperties } from 'react';
|
|
4
4
|
import type { DocsConfig, SocialPlatform, FooterLinkColumn } from '@/lib/docs-types';
|
|
5
5
|
import { getIconClass } from '@/lib/icon-utils';
|
|
6
|
-
import { getBrandingUrl } from '@/lib/branding-url';
|
|
7
|
-
|
|
8
|
-
const showBranding = process.env.NEXT_PUBLIC_SHOW_BRANDING !== 'false';
|
|
6
|
+
import { getBrandingUrl, isBrandingVisible } from '@/lib/branding-url';
|
|
9
7
|
|
|
10
8
|
// Wordmark renders as a CSS mask so its fill inherits `currentColor`, letting
|
|
11
9
|
// the link's text color (and hover state) drive the SVG color in one place.
|
|
@@ -149,9 +147,8 @@ function SocialIcons({ socials }: { socials: Partial<Record<SocialPlatform, stri
|
|
|
149
147
|
}
|
|
150
148
|
|
|
151
149
|
/** "Powered by Jamdesk" attribution link, shared by the full footer and the
|
|
152
|
-
* embed footer.
|
|
150
|
+
* embed footer. Callers decide whether to render it (isBrandingVisible). */
|
|
153
151
|
function BrandingLink({ projectSlug }: { projectSlug?: string }) {
|
|
154
|
-
if (!showBranding) return null;
|
|
155
152
|
return (
|
|
156
153
|
<a
|
|
157
154
|
href={getBrandingUrl(projectSlug)}
|
|
@@ -171,6 +168,8 @@ function BrandingLink({ projectSlug }: { projectSlug?: string }) {
|
|
|
171
168
|
}
|
|
172
169
|
|
|
173
170
|
export function SocialFooter({ config, hidden, projectSlug, embed }: SocialFooterProps) {
|
|
171
|
+
const showBranding = isBrandingVisible(config);
|
|
172
|
+
|
|
174
173
|
// Embed render (widget modal): keep ONLY the "Powered by Jamdesk" attribution.
|
|
175
174
|
// The link columns + social icons read as out of place inside an embedded
|
|
176
175
|
// changelog; the attribution stays even when the normal footer is hidden,
|
|
@@ -202,7 +201,7 @@ export function SocialFooter({ config, hidden, projectSlug, embed }: SocialFoote
|
|
|
202
201
|
{hasLinks && <LinkColumns columns={links} />}
|
|
203
202
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
204
203
|
{hasSocials && <SocialIcons socials={socials} />}
|
|
205
|
-
<BrandingLink projectSlug={projectSlug} />
|
|
204
|
+
{showBranding && <BrandingLink projectSlug={projectSlug} />}
|
|
206
205
|
</div>
|
|
207
206
|
</footer>
|
|
208
207
|
);
|
|
@@ -4,7 +4,8 @@ import { useId, useState } from 'react';
|
|
|
4
4
|
import { useLinkPrefix } from '@/lib/link-prefix-context';
|
|
5
5
|
import { useThemePreview } from '@/lib/theme-preview-context';
|
|
6
6
|
import { applyThemePreview, type ThemePreviewSurface } from '@/lib/theme-preview-client';
|
|
7
|
-
import {
|
|
7
|
+
import { getPreviewThemes } from '@/lib/theme-preview';
|
|
8
|
+
import type { ThemeName } from '@/themes';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Jamdesk-only affordance: re-skins jamdesk.com/docs with any built-in theme
|
|
@@ -52,7 +53,7 @@ export function ThemePreviewChips() {
|
|
|
52
53
|
applyThemePreview(name, { linkPrefix, surface });
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
const themes =
|
|
56
|
+
const themes = getPreviewThemes();
|
|
56
57
|
const previewing = activeTheme !== defaultTheme;
|
|
57
58
|
const activeName = themes.find((t) => t.name === activeTheme)?.displayName ?? activeTheme;
|
|
58
59
|
|
|
@@ -7,3 +7,12 @@ export function getBrandingUrl(projectSlug?: string | null): string {
|
|
|
7
7
|
);
|
|
8
8
|
return `https://www.jamdesk.com?utm_campaign=poweredBy&utm_medium=referral&utm_source=${slug}`;
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
/** Whether to show "Powered by Jamdesk". The build writes `_showBranding: false`
|
|
12
|
+
* into the site config only when the owner hid it, so a missing setting (an
|
|
13
|
+
* older build, local preview) or a missing config keeps the badge. */
|
|
14
|
+
export function isBrandingVisible(
|
|
15
|
+
config: { _showBranding?: boolean } | null | undefined,
|
|
16
|
+
): boolean {
|
|
17
|
+
return config?._showBranding !== false;
|
|
18
|
+
}
|
|
@@ -262,7 +262,7 @@ export function parseErrorDetails(
|
|
|
262
262
|
const requiredFields =
|
|
263
263
|
'Required fields:\n' +
|
|
264
264
|
'• "name": Your site name\n' +
|
|
265
|
-
'• "theme": One of "jam", "nebula", "pulsar", or "
|
|
265
|
+
'• "theme": One of "jam", "nebula", "pulsar", "halo", or "dusk"\n' +
|
|
266
266
|
'• "colors": { "primary": "#hexcolor" }\n' +
|
|
267
267
|
'• "navigation": one of "tabs", "groups", "pages", "dropdowns", ' +
|
|
268
268
|
'"versions", "languages", or "products" ' +
|
|
@@ -23,7 +23,7 @@ export const ASSET_PREFIX = '/_jd';
|
|
|
23
23
|
/**
|
|
24
24
|
* Available themes
|
|
25
25
|
*/
|
|
26
|
-
export type ThemeName = 'jam' | 'nebula' | 'pulsar' | 'halo';
|
|
26
|
+
export type ThemeName = 'jam' | 'nebula' | 'pulsar' | 'halo' | 'dusk';
|
|
27
27
|
|
|
28
28
|
// =============================================================================
|
|
29
29
|
// ICON TYPES
|
|
@@ -973,6 +973,10 @@ export interface DocsConfig {
|
|
|
973
973
|
_hasCustomCss?: boolean;
|
|
974
974
|
_hasCustomJs?: boolean;
|
|
975
975
|
|
|
976
|
+
// Set by the build to `false` only when the owner hid "Powered by Jamdesk"
|
|
977
|
+
// (paid plans). Absent means shown. Read by isBrandingVisible.
|
|
978
|
+
_showBranding?: boolean;
|
|
979
|
+
|
|
976
980
|
// Runtime: Set by ISR middleware based on project config
|
|
977
981
|
// When true, all navigation links should be prefixed with /docs
|
|
978
982
|
hostAtDocs?: boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dusk launch embargo — flip to `true` on October 1, 2026.
|
|
3
|
+
*
|
|
4
|
+
* One of four twins (build-service, CLI, dashboard, marketing); separate apps
|
|
5
|
+
* with separate deploys, so they cannot share a module. Flip all four in one
|
|
6
|
+
* commit. See docs/plans/2026-09-23-dusk-october-1-release-plan.md.
|
|
7
|
+
*
|
|
8
|
+
* Here it gates the jamdesk-docs theme picker, which lists every registry
|
|
9
|
+
* theme and is live on jamdesk.com/docs — without it the next ISR deploy
|
|
10
|
+
* would announce Dusk. The registry, schema and validation are NOT gated: a
|
|
11
|
+
* docs.json that names "dusk" must build (the demo site soaks on it).
|
|
12
|
+
*
|
|
13
|
+
* The `: boolean` annotation is LOAD-BEARING: without it TS narrows to the
|
|
14
|
+
* literal `false` and stops type-checking the launch branch.
|
|
15
|
+
*/
|
|
16
|
+
export const DUSK_PUBLIC: boolean = false;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Email Notifier — sends internal build failure notifications via
|
|
2
|
+
* Email Notifier — sends internal build failure notifications via Mailfully.
|
|
3
3
|
*
|
|
4
|
-
* Env:
|
|
4
|
+
* Env: MAILFULLY_API_KEY, JAMDESK_REPORT_EMAIL
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { render } from '@react-email/render';
|
|
8
|
-
import {
|
|
8
|
+
import { sendMailfullyEmail } from './mailfully.js';
|
|
9
9
|
import { BuildFailureEmail, type BuildFailureEmailProps } from './email-templates/index.js';
|
|
10
10
|
|
|
11
11
|
export type BuildFailureEmailInfo = BuildFailureEmailProps;
|
|
@@ -15,13 +15,13 @@ export type BuildFailureEmailInfo = BuildFailureEmailProps;
|
|
|
15
15
|
* Gracefully skips if environment variables are not configured.
|
|
16
16
|
*/
|
|
17
17
|
export async function sendInternalBuildFailureEmail(info: BuildFailureEmailInfo): Promise<void> {
|
|
18
|
-
const apiKey = process.env.
|
|
18
|
+
const apiKey = process.env.MAILFULLY_API_KEY;
|
|
19
19
|
const reportEmail = process.env.JAMDESK_REPORT_EMAIL;
|
|
20
20
|
|
|
21
21
|
if (!apiKey || !reportEmail) {
|
|
22
22
|
console.log(JSON.stringify({
|
|
23
23
|
severity: 'WARNING',
|
|
24
|
-
message: `Email notification skipped - ${!apiKey ? '
|
|
24
|
+
message: `Email notification skipped - ${!apiKey ? 'MAILFULLY_API_KEY' : 'JAMDESK_REPORT_EMAIL'} not configured`,
|
|
25
25
|
projectId: info.projectId,
|
|
26
26
|
buildId: info.buildId,
|
|
27
27
|
}));
|
|
@@ -29,13 +29,11 @@ export async function sendInternalBuildFailureEmail(info: BuildFailureEmailInfo)
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
try {
|
|
32
|
-
const resend = new Resend(apiKey);
|
|
33
|
-
|
|
34
32
|
const template = BuildFailureEmail(info);
|
|
35
33
|
const html = await render(template);
|
|
36
34
|
const text = await render(template, {plainText: true});
|
|
37
35
|
|
|
38
|
-
const result = await
|
|
36
|
+
const result = await sendMailfullyEmail(apiKey, {
|
|
39
37
|
from: 'Jamdesk <support@jamdesk.com>',
|
|
40
38
|
to: reportEmail,
|
|
41
39
|
subject: `Jamdesk build failed: ${info.projectName || info.projectId}`,
|