jamdesk 1.1.221 → 1.1.222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/__tests__/unit/custom-assets.test.js +157 -6
  2. package/dist/__tests__/unit/custom-assets.test.js.map +1 -1
  3. package/dist/commands/dev.d.ts.map +1 -1
  4. package/dist/commands/dev.js +26 -8
  5. package/dist/commands/dev.js.map +1 -1
  6. package/dist/lib/custom-assets.d.ts +17 -0
  7. package/dist/lib/custom-assets.d.ts.map +1 -1
  8. package/dist/lib/custom-assets.js +109 -1
  9. package/dist/lib/custom-assets.js.map +1 -1
  10. package/package.json +1 -1
  11. package/vendored/app/api/chat/[project]/route.ts +31 -7
  12. package/vendored/components/chat/ChatBrandIcon.tsx +17 -0
  13. package/vendored/components/chat/ChatEmptyState.tsx +5 -5
  14. package/vendored/components/chat/ChatPanel.tsx +10 -7
  15. package/vendored/components/chat/useChatLabel.ts +9 -0
  16. package/vendored/components/layout/LayoutWrapper.tsx +3 -0
  17. package/vendored/components/mdx/Mermaid.tsx +16 -3
  18. package/vendored/components/mdx/MermaidInner.tsx +272 -19
  19. package/vendored/components/mdx/MermaidZoomModal.tsx +87 -0
  20. package/vendored/components/mdx/ZoomableImage.tsx +8 -1
  21. package/vendored/components/navigation/Header.tsx +15 -10
  22. package/vendored/components/navigation/Sidebar.tsx +6 -3
  23. package/vendored/components/navigation/TabsNav.tsx +9 -2
  24. package/vendored/components/search/SearchModal.tsx +6 -3
  25. package/vendored/hooks/useChat.ts +2 -0
  26. package/vendored/hooks/useChatPanel.tsx +9 -2
  27. package/vendored/lib/anthropic-client.ts +7 -0
  28. package/vendored/lib/build/error-parser.ts +17 -0
  29. package/vendored/lib/build/extract-tarball.ts +1 -29
  30. package/vendored/lib/build/validate-no-symlink-escape.ts +100 -0
  31. package/vendored/lib/chat-branding.ts +26 -0
  32. package/vendored/lib/chat-prompt.ts +2 -2
  33. package/vendored/lib/docs-types.ts +20 -0
  34. package/vendored/lib/generate-starter-questions.ts +24 -6
  35. package/vendored/lib/github-git-auth.ts +47 -0
  36. package/vendored/lib/hedge-strip.ts +1 -1
  37. package/vendored/lib/isr-build-executor.ts +63 -12
  38. package/vendored/lib/layout-helpers.tsx +13 -6
  39. package/vendored/lib/link-rewriter.ts +1 -1
  40. package/vendored/lib/mcp-auth.ts +67 -0
  41. package/vendored/lib/openapi/cache.ts +5 -2
  42. package/vendored/lib/openapi/expand-spec-pages.ts +5 -3
  43. package/vendored/lib/openapi/safe-ref-resolvers.ts +455 -0
  44. package/vendored/lib/openapi/validator.ts +46 -9
  45. package/vendored/lib/openapi-isr.ts +24 -11
  46. package/vendored/lib/page-timestamps.ts +29 -6
  47. package/vendored/lib/query-rewriter.ts +7 -3
  48. package/vendored/lib/r2-manifest.ts +2 -0
  49. package/vendored/lib/runtime-flags.ts +54 -0
  50. package/vendored/schema/docs-schema.json +34 -0
  51. package/vendored/scripts/copy-files.cjs +90 -37
  52. package/vendored/scripts/openapi-safe-ref-resolvers.cjs +354 -0
  53. package/vendored/scripts/openapi-spec-loader.cjs +10 -5
  54. package/vendored/themes/base.css +48 -0
  55. package/vendored/workspace-package-lock.json +18 -18
  56. package/vendored/components/chat/ChatFAB.tsx +0 -36
@@ -0,0 +1,354 @@
1
+ // builder/build-service/scripts/openapi-safe-ref-resolvers.cjs
2
+ //
3
+ // CommonJS twin of lib/openapi/safe-ref-resolvers.ts for the build scripts,
4
+ // which run in plain node and are vendored into the CLI, so they cannot
5
+ // import TypeScript (same pairing as lib/visibility-filter.ts /
6
+ // scripts/visibility-filter.cjs). The policy and its reasoning live in the
7
+ // TS file's header. Keep the two identical:
8
+ // __tests__/openapi/safe-ref-resolvers.test.ts runs the same cases on both.
9
+
10
+ const dns = require('node:dns');
11
+ const fs = require('node:fs');
12
+ const http = require('node:http');
13
+ const https = require('node:https');
14
+ const net = require('node:net');
15
+ const path = require('node:path');
16
+ const { fileURLToPath } = require('node:url');
17
+
18
+ const REF_FETCH_TIMEOUT_MS = 10_000;
19
+ const REF_FETCH_MAX_BYTES = 5_000_000;
20
+ const REF_FETCH_MAX_REDIRECTS = 5;
21
+ const SPEC_FETCH_TIMEOUT_MS = 15_000;
22
+ const SPEC_FETCH_MAX_BYTES = 25_000_000;
23
+
24
+ const BLOCKED_ADDRESSES = new net.BlockList();
25
+ for (const [prefix, bits] of [
26
+ ['0.0.0.0', 8],
27
+ ['10.0.0.0', 8],
28
+ ['100.64.0.0', 10],
29
+ ['127.0.0.0', 8],
30
+ ['169.254.0.0', 16],
31
+ ['172.16.0.0', 12],
32
+ ['192.0.0.0', 24],
33
+ ['192.168.0.0', 16],
34
+ ['198.18.0.0', 15],
35
+ ['224.0.0.0', 3],
36
+ ]) {
37
+ BLOCKED_ADDRESSES.addSubnet(prefix, bits, 'ipv4');
38
+ }
39
+ for (const [prefix, bits] of [
40
+ ['::', 96],
41
+ ['64:ff9b::', 96],
42
+ ['64:ff9b:1::', 48],
43
+ ['2002::', 16],
44
+ ['fc00::', 7],
45
+ ['fe80::', 10],
46
+ ['fec0::', 10],
47
+ ['ff00::', 8],
48
+ ]) {
49
+ BLOCKED_ADDRESSES.addSubnet(prefix, bits, 'ipv6');
50
+ }
51
+
52
+ function isBlockedAddress(address) {
53
+ const family = net.isIP(address);
54
+ if (family === 0) return true;
55
+ return BLOCKED_ADDRESSES.check(address, family === 6 ? 'ipv6' : 'ipv4');
56
+ }
57
+
58
+ function bareHostname(url) {
59
+ return url.hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
60
+ }
61
+
62
+ const LOOPBACK_ADDRESSES = new net.BlockList();
63
+ LOOPBACK_ADDRESSES.addSubnet('127.0.0.0', 8, 'ipv4');
64
+ LOOPBACK_ADDRESSES.addAddress('::1', 'ipv6');
65
+
66
+ function isLoopbackAddress(address) {
67
+ const family = net.isIP(address);
68
+ return family !== 0 && LOOPBACK_ADDRESSES.check(address, family === 6 ? 'ipv6' : 'ipv4');
69
+ }
70
+
71
+ /** `localhost`, `*.localhost`, 127.0.0.0/8 and ::1 (IP literals arrive normalized). */
72
+ function isLoopbackHost(host) {
73
+ return host === 'localhost' || host.endsWith('.localhost') || isLoopbackAddress(host);
74
+ }
75
+
76
+ function checkRefUrl(rawUrl, { allowHttp = false, allowLoopback = false } = {}) {
77
+ let url;
78
+ try {
79
+ url = new URL(rawUrl);
80
+ } catch {
81
+ return 'not a valid URL';
82
+ }
83
+ if (url.protocol !== 'https:' && !(allowHttp && url.protocol === 'http:')) {
84
+ return allowHttp ? 'only http:// and https:// URLs are allowed' : 'only https:// $refs are allowed';
85
+ }
86
+ const host = bareHostname(url);
87
+ if (allowLoopback && isLoopbackHost(host)) return null;
88
+ if (net.isIP(host) !== 0) return 'IP address hosts are not allowed';
89
+ if (
90
+ host === 'localhost' ||
91
+ host.endsWith('.localhost') ||
92
+ host.endsWith('.internal') ||
93
+ host.endsWith('.local') ||
94
+ !host.includes('.')
95
+ ) {
96
+ return 'internal hosts are not allowed';
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function lookupPublicAddresses(hostname, signal, allowLoopback) {
102
+ return new Promise((resolve, reject) => {
103
+ const onAbort = () => reject(new Error(`DNS lookup for ${hostname} timed out`));
104
+ if (signal.aborted) return onAbort();
105
+ signal.addEventListener('abort', onAbort, { once: true });
106
+ dns.lookup(hostname, { all: true }, (err, addresses) => {
107
+ signal.removeEventListener('abort', onAbort);
108
+ if (err) return reject(err);
109
+ if (addresses.length === 0 || addresses.some((a) => !(allowLoopback && isLoopbackAddress(a.address)) && isBlockedAddress(a.address))) {
110
+ return reject(new Error(`${hostname} resolves to a non-public address`));
111
+ }
112
+ resolve(addresses);
113
+ });
114
+ });
115
+ }
116
+
117
+ // Hosts like api.github.com and some WAF bot rules 403 a request with no
118
+ // User-Agent, and node:http adds none (the old runtime fetch sent undici's).
119
+ // Identity encoding because this fetcher does not decode compressed bodies.
120
+ const REQUEST_HEADERS = (accept) => ({
121
+ Accept: accept,
122
+ 'User-Agent': 'Jamdesk-OpenAPI-Fetcher/1.0',
123
+ 'Accept-Encoding': 'identity',
124
+ });
125
+
126
+ function requestOnce(url, addresses, signal, maxBytes, accept) {
127
+ return new Promise((resolve, reject) => {
128
+ const lookup = (_hostname, options, callback) => {
129
+ if (options.all) callback(null, addresses);
130
+ else callback(null, addresses[0].address, addresses[0].family);
131
+ };
132
+
133
+ const transport = url.protocol === 'http:' ? http : https;
134
+ const req = transport.request(
135
+ url,
136
+ { method: 'GET', agent: false, lookup, signal, headers: REQUEST_HEADERS(accept) },
137
+ (res) => {
138
+ const status = res.statusCode ?? 0;
139
+ if (status >= 300 && status < 400) {
140
+ res.resume();
141
+ resolve({ location: res.headers.location });
142
+ return;
143
+ }
144
+ if (status < 200 || status >= 300) {
145
+ res.resume();
146
+ reject(new Error(`HTTP ${status}`));
147
+ return;
148
+ }
149
+ if (Number(res.headers['content-length']) > maxBytes) {
150
+ res.destroy();
151
+ reject(new Error(`response exceeds ${maxBytes} bytes`));
152
+ return;
153
+ }
154
+ const chunks = [];
155
+ let size = 0;
156
+ res.on('data', (chunk) => {
157
+ size += chunk.length;
158
+ if (size > maxBytes) {
159
+ res.destroy();
160
+ reject(new Error(`response exceeds ${maxBytes} bytes`));
161
+ return;
162
+ }
163
+ chunks.push(chunk);
164
+ });
165
+ res.on('end', () => resolve({ body: Buffer.concat(chunks) }));
166
+ res.on('error', reject);
167
+ },
168
+ );
169
+ req.on('error', reject);
170
+ signal.addEventListener('abort', () => reject(new Error('timed out')), { once: true });
171
+ req.end();
172
+ });
173
+ }
174
+
175
+ function startsAtLoopback(rawUrl) {
176
+ try {
177
+ return isLoopbackHost(bareHostname(new URL(rawUrl)));
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
182
+
183
+ async function fetchRefSafely(
184
+ rawUrl,
185
+ {
186
+ timeoutMs = REF_FETCH_TIMEOUT_MS,
187
+ maxBytes = REF_FETCH_MAX_BYTES,
188
+ allowHttp = false,
189
+ allowLoopback = false,
190
+ accept = '*/*',
191
+ } = {},
192
+ ) {
193
+ const signal = AbortSignal.timeout(timeoutMs);
194
+ // Loopback is only for a spec the author pointed at their own machine. A
195
+ // public URL that redirects to localhost must not get there.
196
+ const loopbackOk = allowLoopback && startsAtLoopback(rawUrl);
197
+ let current = rawUrl;
198
+ for (let hop = 0; hop <= REF_FETCH_MAX_REDIRECTS; hop++) {
199
+ const reason = checkRefUrl(current, { allowHttp, allowLoopback: loopbackOk });
200
+ if (reason) throw new Error(`Refusing $ref ${current}: ${reason}`);
201
+ const url = new URL(current);
202
+ const addresses = await lookupPublicAddresses(bareHostname(url), signal, loopbackOk);
203
+ const result = await requestOnce(url, addresses, signal, maxBytes, accept);
204
+ if (result.body) return result.body;
205
+ if (!result.location) throw new Error(`Redirect from ${current} has no location`);
206
+ current = new URL(result.location, url).href;
207
+ }
208
+ throw new Error(`Too many redirects resolving $ref ${rawUrl}`);
209
+ }
210
+
211
+ function fetchSpecUrlSafely(rawUrl) {
212
+ return fetchRefSafely(rawUrl, {
213
+ timeoutMs: SPEC_FETCH_TIMEOUT_MS,
214
+ maxBytes: SPEC_FETCH_MAX_BYTES,
215
+ allowHttp: true,
216
+ // `jamdesk dev` only: an author previewing against their own local API.
217
+ // Never true for a $ref, and the CLI is the only thing that sets the env.
218
+ allowLoopback: process.env.JAMDESK_LOCAL_DEV === '1',
219
+ accept: 'application/json',
220
+ });
221
+ }
222
+
223
+ function isFileRef(url) {
224
+ const match = /^(\w{2,}):\/\//.exec(url);
225
+ return !match || match[1].toLowerCase() === 'file';
226
+ }
227
+
228
+ function toFileSystemPath(url) {
229
+ return /^file:\/\//i.test(url) ? fileURLToPath(url) : decodeURIComponent(url);
230
+ }
231
+
232
+ /**
233
+ * Whether `target` is strictly inside `dir` (both absolute; pass realpaths to
234
+ * stop symlink hops). Same helper as copy-files.cjs. The TS file uses
235
+ * shared/path-security.ts isPathWithinProject, which this CJS file cannot
236
+ * import.
237
+ */
238
+ function isRealPathInside(dir, target) {
239
+ const rel = path.relative(dir, target);
240
+ return rel !== '' && rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
241
+ }
242
+
243
+ function hasGitSegment(relativePath) {
244
+ return relativePath.split(/[\\/]/).some((segment) => segment.toLowerCase() === '.git');
245
+ }
246
+
247
+ // Same lexical rules as shared/path-security.ts isPathWithinProject (which
248
+ // this file cannot import), but strict: the boundary itself is not a file.
249
+ function isLexicallyInside(target, boundaryDir) {
250
+ if (target.includes('\0') || target.includes('%00')) return false;
251
+ if (/%2f/i.test(target) || /%5c/i.test(target)) return false;
252
+ const lexicalBoundary = path.resolve(boundaryDir);
253
+ return isRealPathInside(lexicalBoundary, path.resolve(lexicalBoundary, target.replace(/\\/g, '/')));
254
+ }
255
+
256
+ function isReadableProjectFile(target, boundaryDir) {
257
+ if (!isLexicallyInside(target, boundaryDir)) return false;
258
+ const lexicalBoundary = path.resolve(boundaryDir);
259
+ const lexicalTarget = path.resolve(lexicalBoundary, target.replace(/\\/g, '/'));
260
+ if (hasGitSegment(path.relative(lexicalBoundary, lexicalTarget))) return false;
261
+ let realTarget;
262
+ let realBoundary;
263
+ try {
264
+ realTarget = fs.realpathSync(target);
265
+ realBoundary = fs.realpathSync(boundaryDir);
266
+ } catch {
267
+ return false;
268
+ }
269
+ return isRealPathInside(realBoundary, realTarget) && !hasGitSegment(path.relative(realBoundary, realTarget));
270
+ }
271
+
272
+ function refusedFileRef(target, boundaryDir) {
273
+ const rel = path.relative(path.resolve(boundaryDir), path.resolve(target)).split(path.sep).join('/');
274
+ return new Error(`Cannot resolve $ref: "${rel}" (missing, outside the project, or inside .git)`);
275
+ }
276
+
277
+ function createProjectFileResolver(boundaryDir) {
278
+ return {
279
+ order: 100,
280
+ canRead: (file) => isFileRef(file.url),
281
+ read: async (file) => {
282
+ const target = toFileSystemPath(file.url);
283
+ if (!isReadableProjectFile(target, boundaryDir)) throw refusedFileRef(target, boundaryDir);
284
+ try {
285
+ return await fs.promises.readFile(fs.realpathSync(target));
286
+ } catch {
287
+ throw refusedFileRef(target, boundaryDir);
288
+ }
289
+ },
290
+ };
291
+ }
292
+
293
+ const RENDER_FILE_REF_MESSAGE = 'Cannot resolve $ref: file $refs are not resolved at render time';
294
+
295
+ function createRefuseFileResolver() {
296
+ return {
297
+ order: 100,
298
+ canRead: (file) => isFileRef(file.url),
299
+ read: () => {
300
+ throw new Error(RENDER_FILE_REF_MESSAGE);
301
+ },
302
+ };
303
+ }
304
+
305
+ function sanitizeRefError(err, boundaryDir) {
306
+ if (!(err instanceof Error)) return err;
307
+ const match = /^(?:Error reading file|Unable to resolve \$ref pointer) "(.+)"$/.exec(err.message);
308
+ const url = match && match[1];
309
+ if (!url || !isFileRef(url)) return err;
310
+ if (!boundaryDir) return new Error(RENDER_FILE_REF_MESSAGE);
311
+ let target = url;
312
+ try {
313
+ target = toFileSystemPath(url);
314
+ } catch {
315
+ // Malformed percent-encoding: judge the raw string.
316
+ }
317
+ return refusedFileRef(target, boundaryDir);
318
+ }
319
+
320
+ function createSafeHttpResolver() {
321
+ return {
322
+ order: 200,
323
+ canRead: (file) => /^https?:\/\//i.test(file.url),
324
+ read: (file) => fetchRefSafely(file.url),
325
+ };
326
+ }
327
+
328
+ function safeRefParserOptions(boundaryDir) {
329
+ return {
330
+ resolve: {
331
+ file: false,
332
+ http: false,
333
+ ...(boundaryDir
334
+ ? { projectFile: createProjectFileResolver(boundaryDir) }
335
+ : { refuseFile: createRefuseFileResolver() }),
336
+ safeHttps: createSafeHttpResolver(),
337
+ },
338
+ };
339
+ }
340
+
341
+ module.exports = {
342
+ REF_FETCH_TIMEOUT_MS,
343
+ REF_FETCH_MAX_BYTES,
344
+ SPEC_FETCH_TIMEOUT_MS,
345
+ SPEC_FETCH_MAX_BYTES,
346
+ isBlockedAddress,
347
+ checkRefUrl,
348
+ fetchRefSafely,
349
+ fetchSpecUrlSafely,
350
+ isReadableProjectFile,
351
+ isRealPathInside,
352
+ safeRefParserOptions,
353
+ sanitizeRefError,
354
+ };
@@ -7,8 +7,8 @@
7
7
  // as lib/visibility-filter.ts / scripts/visibility-filter.cjs.
8
8
 
9
9
  const path = require('path');
10
- const fs = require('fs');
11
10
  const SwaggerParser = require('@apidevtools/swagger-parser');
11
+ const { isReadableProjectFile, safeRefParserOptions } = require('./openapi-safe-ref-resolvers.cjs');
12
12
 
13
13
  const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
14
14
 
@@ -22,10 +22,15 @@ function createSpecLoader(docsDir) {
22
22
  const promise = (async () => {
23
23
  try {
24
24
  const full = path.resolve(docsDir, key);
25
- if (!fs.existsSync(full)) return null;
26
- // dereference() inlines every $ref. NOTE: for recursive schemas this
27
- // produces genuinely circular JS objects — consumers MUST guard.
28
- return await SwaggerParser.dereference(full);
25
+ // Frontmatter spec paths are customer input: out of docsDir, into .git
26
+ // or missing all read as missing. docsDir, not a repo root: these
27
+ // scripts run under the CLI, which has no notion of one.
28
+ if (!isReadableProjectFile(full, docsDir)) return null;
29
+ // dereference() inlines every $ref (file refs only inside docsDir,
30
+ // external refs only via the SSRF-safe https resolver). NOTE: for
31
+ // recursive schemas this produces genuinely circular JS objects —
32
+ // consumers MUST guard.
33
+ return await SwaggerParser.dereference(full, safeRefParserOptions(docsDir));
29
34
  } catch {
30
35
  return null;
31
36
  }
@@ -1182,6 +1182,54 @@ img.center {
1182
1182
  box-shadow: var(--shadow-2xl);
1183
1183
  }
1184
1184
 
1185
+ /* Mermaid zoom (MermaidZoomModal) reuses the image modal and backdrop above. */
1186
+ /* Scrollable panel: a wide diagram fits the width, a tall one keeps its
1187
+ readable size and scrolls instead of shrinking to fit 90vh. */
1188
+ .mermaid-zoom-panel {
1189
+ position: relative;
1190
+ width: 90vw;
1191
+ max-height: 90vh;
1192
+ overflow: auto;
1193
+ background: var(--color-bg-primary);
1194
+ border-radius: var(--radius-md);
1195
+ box-shadow: var(--shadow-2xl);
1196
+ padding: 1.5rem;
1197
+ cursor: auto;
1198
+ }
1199
+
1200
+ /* MermaidZoomModal caps this inline at 1.5x the diagram's natural width. */
1201
+ .mermaid-zoom-content {
1202
+ margin: 0 auto;
1203
+ }
1204
+
1205
+ /* Beats mermaid's inline max-width so the viewBox scales to the content width. */
1206
+ .mermaid-zoom-panel svg {
1207
+ display: block;
1208
+ width: 100% !important;
1209
+ height: auto !important;
1210
+ max-width: none !important;
1211
+ min-width: 0 !important;
1212
+ }
1213
+
1214
+ .mermaid-zoom-close {
1215
+ position: sticky;
1216
+ top: 0;
1217
+ float: right;
1218
+ z-index: 1;
1219
+ display: inline-flex;
1220
+ align-items: center;
1221
+ justify-content: center;
1222
+ width: 2rem;
1223
+ height: 2rem;
1224
+ border-radius: var(--radius-md);
1225
+ background: var(--color-bg-primary);
1226
+ color: var(--color-text-secondary);
1227
+ }
1228
+
1229
+ .mermaid-zoom-close:hover {
1230
+ color: var(--color-text-primary);
1231
+ }
1232
+
1185
1233
  /* ===== IFRAME STYLES ===== */
1186
1234
  iframe {
1187
1235
  border: none !important;
@@ -2153,9 +2153,9 @@
2153
2153
  }
2154
2154
  },
2155
2155
  "node_modules/@types/node": {
2156
- "version": "26.6.4",
2157
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz",
2158
- "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==",
2156
+ "version": "26.6.5",
2157
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
2158
+ "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
2159
2159
  "license": "MIT",
2160
2160
  "dependencies": {
2161
2161
  "undici-types": "~8.9.0"
@@ -2449,9 +2449,9 @@
2449
2449
  }
2450
2450
  },
2451
2451
  "node_modules/baseline-browser-mapping": {
2452
- "version": "2.11.27",
2453
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz",
2454
- "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==",
2452
+ "version": "2.11.28",
2453
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz",
2454
+ "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==",
2455
2455
  "license": "Apache-2.0",
2456
2456
  "bin": {
2457
2457
  "baseline-browser-mapping": "dist/cli.cjs"
@@ -3279,9 +3279,9 @@
3279
3279
  "license": "MIT"
3280
3280
  },
3281
3281
  "node_modules/electron-to-chromium": {
3282
- "version": "1.5.449",
3283
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.449.tgz",
3284
- "integrity": "sha512-dKeYk/3SFQz+nXi5whp3OuJzeWABEG4zCtkWJ9+Vby1E1iFM2y0gElRtQvNXRPJTsj8WAF4HMP7hvP790QtJ9g==",
3282
+ "version": "1.5.452",
3283
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.452.tgz",
3284
+ "integrity": "sha512-cVvk1YBSj6571VmV6hdJTqj94Y79xSu8cj6Y9t6ae4Y4431z8/7EJFvu3rVp90xhfuv9BpPKUmaLfTo4+FOGBg==",
3285
3285
  "license": "ISC"
3286
3286
  },
3287
3287
  "node_modules/elkjs": {
@@ -5968,9 +5968,9 @@
5968
5968
  }
5969
5969
  },
5970
5970
  "node_modules/node-releases": {
5971
- "version": "2.0.57",
5972
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz",
5973
- "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==",
5971
+ "version": "2.0.58",
5972
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz",
5973
+ "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==",
5974
5974
  "license": "MIT",
5975
5975
  "engines": {
5976
5976
  "node": ">=18"
@@ -7186,9 +7186,9 @@
7186
7186
  }
7187
7187
  },
7188
7188
  "node_modules/update-browserslist-db": {
7189
- "version": "1.3.3",
7190
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",
7191
- "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==",
7189
+ "version": "1.3.4",
7190
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz",
7191
+ "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==",
7192
7192
  "funding": [
7193
7193
  {
7194
7194
  "type": "opencollective",
@@ -7222,9 +7222,9 @@
7222
7222
  "license": "MIT"
7223
7223
  },
7224
7224
  "node_modules/uuid": {
7225
- "version": "14.0.2",
7226
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz",
7227
- "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==",
7225
+ "version": "14.0.3",
7226
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.3.tgz",
7227
+ "integrity": "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw==",
7228
7228
  "funding": [
7229
7229
  "https://github.com/sponsors/broofa",
7230
7230
  "https://github.com/sponsors/ctavan"
@@ -1,36 +0,0 @@
1
- 'use client';
2
-
3
- interface ChatFABProps {
4
- isOpen: boolean;
5
- onClick: () => void;
6
- }
7
-
8
- /**
9
- * Floating action button that opens the AI chat panel.
10
- * Pill-shaped with accent color, slides down/fades when chat panel is open.
11
- * Positioned above common third-party chat widgets (Crisp, Intercom) which
12
- * sit at bottom-right with z-index ~1,000,000+. We use z-[1000003] to ensure
13
- * clickability and bottom-20 to avoid visual overlap.
14
- */
15
- export function ChatFAB({ isOpen, onClick }: ChatFABProps) {
16
- return (
17
- <button
18
- onClick={onClick}
19
- className={`
20
- fixed bottom-20 right-6 z-[1000003]
21
- flex items-center gap-2 px-4 py-2.5
22
- bg-[var(--color-accent)] text-white
23
- rounded-full shadow-lg
24
- transition-all duration-200
25
- hover:shadow-xl hover:scale-105 cursor-pointer
26
- focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]
27
- ${isOpen ? 'translate-y-16 opacity-0 pointer-events-none' : 'translate-y-0 opacity-100'}
28
- `}
29
- aria-label="Open AI chat"
30
- title="Ask AI"
31
- >
32
- <i className="fa-solid fa-comment text-sm" aria-hidden="true" />
33
- <span className="text-sm font-medium">Ask AI</span>
34
- </button>
35
- );
36
- }