Compare commits

...

6 Commits

Author SHA1 Message Date
Nate Kelley d443c1c333
increase resolution slightly 2025-10-08 23:24:50 -06:00
Nate Kelley fda1878f09
fix skeleton loader 2025-10-08 23:08:09 -06:00
Nate Kelley a01ee7fa70
standard for generate is png, standard for screenshot is png 2025-10-08 22:59:10 -06:00
Nate Kelley dbc85f9a42
use webp 2025-10-08 22:51:21 -06:00
Nate Kelley 04287fe4c4
better mins and maxes 2025-10-08 22:28:47 -06:00
Nate Kelley b5076b3079
use default widths 2025-10-08 22:01:47 -06:00
32 changed files with 433 additions and 232 deletions

View File

@ -16,7 +16,6 @@ const app = new Hono().get(
zValidator('query', GetChatScreenshotQuerySchema),
async (c) => {
const chatId = c.req.valid('param').id;
const search = c.req.valid('query');
const user = c.get('busterUser');
const chat = await getChatById(chatId);
@ -40,16 +39,15 @@ const app = new Hono().get(
}
try {
const type = 'png' as const;
const screenshotBuffer = await getChatScreenshot({
chatId,
width: search.width,
height: search.height,
type: search.type,
accessToken: c.get('accessToken'),
organizationId: chat.organizationId,
type,
});
return createImageResponse(screenshotBuffer, search.type);
return createImageResponse(screenshotBuffer, type);
} catch (error) {
console.error('Failed to generate chat screenshot URL', {
chatId,

View File

@ -6,6 +6,7 @@ import {
getCollectionsAssociatedWithDashboard,
getDashboardById,
getOrganizationMemberCount,
getUserOrganizationId,
getUsersWithAssetPermissions,
} from '@buster/database/queries';
import {
@ -54,6 +55,27 @@ const app = new Hono().get(
c
);
const tag = `take-dashboard-screenshot-${id}`;
if (
await shouldTakeScreenshot({
tag,
key: screenshots_task_keys.take_dashboard_screenshot,
context: c,
})
) {
console.log('Taking dashboard screenshot');
tasks.trigger(
screenshots_task_keys.take_dashboard_screenshot,
{
dashboardId: id,
organizationId: (await getUserOrganizationId(user.id))?.organizationId || '',
accessToken: c.get('accessToken'),
isOnSaveEvent: false,
} satisfies TakeDashboardScreenshotTrigger,
{ tags: [tag] }
);
}
return c.json(response);
}
);

View File

@ -43,14 +43,16 @@ const app = new Hono()
}
try {
const type = 'png' as const;
const screenshotBuffer = await getDashboardScreenshot({
...search,
dashboardId,
accessToken: c.get('accessToken'),
organizationId: dashboard.organizationId,
type,
});
return createImageResponse(screenshotBuffer, search.type);
return createImageResponse(screenshotBuffer, type);
} catch (error) {
console.error('Failed to generate chat screenshot URL', {
dashboardId,

View File

@ -18,7 +18,7 @@ const app = new Hono()
zValidator('query', GetMetricScreenshotQuerySchema),
async (c) => {
const metricId = c.req.valid('param').id;
const { version_number, width, height, type } = c.req.valid('query');
const { version_number } = c.req.valid('query');
const user = c.get('busterUser');
const metric = await getMetricFileById(metricId);
@ -42,14 +42,13 @@ const app = new Hono()
}
try {
const type = 'png' as const;
const screenshotBuffer = await getMetricScreenshot({
metricId,
width,
height,
version_number,
type,
accessToken: c.get('accessToken'),
organizationId: metric.organizationId,
type,
});
return createImageResponse(screenshotBuffer, type);

View File

@ -1,13 +1,8 @@
import { Hono } from 'hono';
import GET from './GET';
import PUT from './PUT';
import SCREENSHOT from './screenshot';
import SHARING from './sharing';
const app = new Hono()
.route('/', GET)
.route('/', PUT)
.route('/sharing', SHARING)
.route('/screenshot', SCREENSHOT);
const app = new Hono().route('/', GET).route('/', PUT).route('/sharing', SHARING);
export default app;

View File

@ -1,75 +0,0 @@
import { checkPermission } from '@buster/access-controls';
import { getAssetScreenshotBucketKey, getReportFileById } from '@buster/database/queries';
import { getAssetScreenshotSignedUrl } from '@buster/search';
import {
GetReportScreenshotParamsSchema,
type GetScreenshotResponse,
} from '@buster/server-shared/screenshots';
import { zValidator } from '@hono/zod-validator';
import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
const app = new Hono().get('/', zValidator('param', GetReportScreenshotParamsSchema), async (c) => {
const reportId = c.req.valid('param').id;
const user = c.get('busterUser');
const report = await getReportFileById({ reportId, userId: user.id });
if (!report) {
throw new HTTPException(404, { message: 'Report not found' });
}
const existingKey = await getAssetScreenshotBucketKey({
assetType: 'report_file',
assetId: reportId,
});
if (!existingKey) {
const result: GetScreenshotResponse = {
success: false,
error: 'Screenshot not found',
};
return c.json(result);
}
const permission = await checkPermission({
userId: user.id,
assetId: reportId,
assetType: 'report_file',
requiredRole: 'can_view',
workspaceSharing: report.workspace_sharing,
organizationId: report.organization_id,
});
if (!permission.hasAccess) {
throw new HTTPException(403, {
message: 'You do not have permission to view this report',
});
}
try {
const signedUrl = await getAssetScreenshotSignedUrl({
key: existingKey,
organizationId: report.organization_id,
});
const result: GetScreenshotResponse = {
success: true,
url: signedUrl,
};
return c.json(result);
} catch (error) {
console.error('Failed to generate report screenshot URL', {
reportId,
error,
});
}
const response: GetScreenshotResponse = {
success: false,
error: 'Failed to generate screenshot URL',
};
return c.json(response);
});
export default app;

View File

@ -1,6 +0,0 @@
import { Hono } from 'hono';
import GET from './GET';
const app = new Hono().route('/', GET);
export default app;

View File

@ -6,7 +6,7 @@ import type { Context } from 'hono';
// It checks if a job for the given tag and key is already running or queued before starting a new one.
const currentlyCheckingTags = new Set<string>();
const CACHE_TAG_EXPIRATION_TIME = 1000 * 15; // 15 seconds
const CACHE_TAG_EXPIRATION_TIME = 1000 * 30; // 30 seconds
export const shouldTakeScreenshot = async ({
tag,

View File

@ -53,10 +53,12 @@ const shouldTakeChatScreenshot = async (
return true;
}
const isScreenshotExpired = await hasChatScreenshotBeenTakenWithin(
const hasRecentScreenshot = await hasChatScreenshotBeenTakenWithin(
args.chatId,
dayjs().subtract(4, 'weeks')
);
return !isScreenshotExpired;
logger.info('Has recent screenshot', { hasRecentScreenshot });
return !hasRecentScreenshot;
};

View File

@ -25,6 +25,8 @@ export const takeDashboardScreenshotHandlerTask: ReturnType<
isOnSaveEvent,
});
logger.info('Should take new screenshot', { shouldTakeNewScreenshot });
if (!shouldTakeNewScreenshot) {
return;
}
@ -54,10 +56,12 @@ const shouldTakenNewScreenshot = async ({
return true;
}
const isScreenshotExpired = await hasDashboardScreenshotBeenTakenWithin(
const hasRecentScreenshot = await hasDashboardScreenshotBeenTakenWithin(
dashboardId,
dayjs().subtract(24, 'hours')
);
return !isScreenshotExpired;
logger.info('Is screenshot expired', { hasRecentScreenshot });
return !hasRecentScreenshot;
};

View File

@ -15,7 +15,7 @@ export const takeMetricScreenshotHandlerTask: ReturnType<
> = schemaTask({
id: screenshots_task_keys.take_metric_screenshot,
schema: TakeMetricScreenshotTriggerSchema,
maxDuration: 60 * 3, // 3 minutes max
maxDuration: 60 * 2, // 2 minutes max
retry: {
maxAttempts: 1,
minTimeoutInMs: 1000, // 1 second

View File

@ -46,10 +46,10 @@ export const takeReportScreenshotHandlerTask: ReturnType<
});
const shouldTakenNewScreenshot = async ({ reportId }: { reportId: string }) => {
const isScreenshotExpired = await hasReportScreenshotBeenTakenWithin(
const hasRecentScreenshot = await hasReportScreenshotBeenTakenWithin(
reportId,
dayjs().subtract(24, 'hours')
);
return !isScreenshotExpired;
return !hasRecentScreenshot;
};

View File

@ -1,4 +1,9 @@
import type { DashboardConfig, GetDashboardResponse } from '@buster/server-shared/dashboards';
import type {
DashboardConfig,
GetDashboardParams,
GetDashboardQuery,
GetDashboardResponse,
} from '@buster/server-shared/dashboards';
import type {
ShareDeleteRequest,
ShareDeleteResponse,
@ -26,15 +31,8 @@ export const getDashboardById = async ({
id,
password,
version_number,
}: {
/** The unique identifier of the dashboard */
id: string;
/** Optional password for accessing protected dashboards */
password?: string;
/** The version number of the dashboard */
version_number?: number;
}) => {
return await mainApi
}: GetDashboardParams & GetDashboardQuery) => {
return await mainApiV2
.get<GetDashboardResponse>(`/dashboards/${id}`, {
params: { password, version_number },
})

View File

@ -45,6 +45,11 @@ export const GlobalSearchModalBase = ({
const navigate = useNavigate();
const [viewedItem, setViewedItem] = useState<SearchTextData | null>(null);
const resetModal = () => {
setViewedItem(null);
onChangeValue('');
};
const searchItems: SearchItems[] = useMemo(() => {
const makeItem = (item: SearchTextData, makeSecondary?: boolean): SearchItem => {
const Icon = assetTypeToIcon(item.assetType);
@ -68,6 +73,9 @@ export const GlobalSearchModalBase = ({
}) as Parameters<typeof navigate>[0];
await navigate(link);
onClose();
setTimeout(() => {
resetModal();
}, 200);
},
};
};

View File

@ -28,8 +28,8 @@ const editorVariants = cva(
},
variant: {
comment: cn('rounded-none border-none bg-transparent text-sm'),
default: 'px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
fullWidth: 'px-16 pt-4 pb-72 text-base sm:px-24',
default: 'px-16 pt-4 pb-72 text-base',
fullWidth: 'pt-4 pb-72 text-base px-24',
none: '',
},
},

View File

@ -55,7 +55,11 @@ export function ReportEditorSkeleton({
}: ReportEditorSkeletonProps) {
return (
<div
className={cn('mx-auto mt-8 w-full space-y-6 sm:px-[max(64px,calc(50%-350px))]', className)}
className={cn(
'mx-auto mt-8 w-full space-y-6',
'sm:px-[max(64px,calc(50%-350px))] px-[max(24px,calc(50%-350px))]',
className
)}
>
{/* Toolbar skeleton */}
{showToolbar && (

View File

@ -1,47 +0,0 @@
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { PlateStatic, type PlateStaticProps } from 'platejs';
import * as React from 'react';
import { cn } from '@/lib/utils';
export const editorVariants = cva(
cn(
'group/editor',
'relative w-full cursor-text overflow-x-hidden break-words whitespace-pre-wrap select-text',
'rounded ring-offset-background focus-visible:outline-none',
'placeholder:text-muted-foreground/80 **:data-slate-placeholder:top-[auto_!important] **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!',
'[&_strong]:font-semibold'
),
{
defaultVariants: {
variant: 'none',
},
variants: {
disabled: {
true: 'cursor-not-allowed opacity-50',
},
focused: {
true: 'ring-2 ring-ring ring-offset-2',
},
variant: {
ai: 'w-full px-0 text-base md:text-sm',
aiChat:
'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-5 py-3 text-base md:text-sm',
default: 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
demo: 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
fullWidth: 'size-full px-16 pt-4 pb-72 text-base sm:px-24',
none: '',
select: 'px-3 py-2 text-base data-readonly:w-fit',
},
},
}
);
export function EditorStatic({
className,
variant,
...props
}: PlateStaticProps & VariantProps<typeof editorVariants>) {
return <PlateStatic className={cn(editorVariants({ variant }), className)} {...props} />;
}

View File

@ -16,7 +16,7 @@ import { useGetCurrentMessageId, useIsStreamingMessage } from '../../context/Cha
import { GeneratingContent } from './GeneratingContent';
import { ReportPageHeader } from './ReportPageHeader';
const commonClassName = 'sm:px-[max(64px,calc(50%-350px))]';
const commonClassName = 'sm:px-[max(64px,calc(50%-350px))] px-[max(24px,calc(50%-350px))]';
export const ReportPageController: React.FC<{
reportId: string;
@ -125,7 +125,7 @@ export const ReportPageController: React.FC<{
}
/>
) : (
<ReportEditorSkeleton />
<ReportEditorSkeleton className={commonClassName} />
)}
</div>
);

View File

@ -18,7 +18,7 @@ export const Route = createFileRoute('/screenshots')({
head: ({ loaderData }) => ({
styles: [
{
children: `body { background: ${loaderData?.backgroundColor || '#ffffff'}; }`,
children: `body { background: ${loaderData?.backgroundColor || '#ffffff'}; min-width: auto; }`,
},
],
}),

View File

@ -136,6 +136,7 @@
"@buster/vitest-config": "workspace:*",
"@supabase/supabase-js": "catalog:",
"playwright": "^1.55.1",
"sharp": "^0.34.4",
"zod": "catalog:"
},
"devDependencies": {

View File

@ -1,4 +1,3 @@
import type { User } from '@supabase/supabase-js';
import type { Browser, Page } from 'playwright';
import { z } from 'zod';
import { getSupabaseCookieKey, getSupabaseUser } from '../../supabase/server';
@ -8,12 +7,25 @@ type BrowserParamsBase<T> = {
width?: number | undefined;
height?: number | undefined;
fullPath: string;
callback: ({ page, browser }: { page: Page; browser: Browser }) => Promise<T>;
callback: ({
page,
browser,
type,
}: { page: Page; browser: Browser; type: 'png' | 'webp' }) => Promise<T>;
};
export const BrowserParamsContextSchema = z.object({
accessToken: z.string(),
organizationId: z.string(),
width: z.number().min(100).max(3840).default(DEFAULT_SCREENSHOT_CONFIG.width).optional(),
height: z.number().min(100).max(7000).default(DEFAULT_SCREENSHOT_CONFIG.height).optional(),
deviceScaleFactor: z
.number()
.min(1)
.max(4)
.default(DEFAULT_SCREENSHOT_CONFIG.deviceScaleFactor)
.optional(),
type: z.enum(['png', 'webp']).default(DEFAULT_SCREENSHOT_CONFIG.type).optional(),
});
export type BrowserParamsContext = z.infer<typeof BrowserParamsContextSchema>;
@ -27,6 +39,8 @@ export const browserLogin = async <T = Buffer<ArrayBufferLike>>({
fullPath,
callback,
accessToken,
deviceScaleFactor,
type,
}: BrowserParams<T>) => {
if (!accessToken) {
throw new Error('Missing Authorization header');
@ -62,6 +76,7 @@ export const browserLogin = async <T = Buffer<ArrayBufferLike>>({
try {
const context = await browser.newContext({
viewport: { width, height },
deviceScaleFactor: deviceScaleFactor || DEFAULT_SCREENSHOT_CONFIG.deviceScaleFactor, // High-DPI rendering for better quality screenshots
});
// Format cookie value as Supabase expects: base64-<encoded_session>
@ -93,7 +108,7 @@ export const browserLogin = async <T = Buffer<ArrayBufferLike>>({
await page.goto(fullPath, { waitUntil: 'networkidle' });
const result = await callback({ page, browser });
const result = await callback({ page, browser, type: type || DEFAULT_SCREENSHOT_CONFIG.type });
if (pageError) {
throw pageError;

View File

@ -1,32 +1,24 @@
import { z } from 'zod';
import { BrowserParamsContextSchema, browserLogin } from './browser-login';
import { createHrefFromLink } from './create-href-from-link';
import { DEFAULT_SCREENSHOT_CONFIG } from './screenshot-config';
import { takeScreenshot } from './take-screenshot';
export const GetChatScreenshotHandlerArgsSchema = z
.object({
chatId: z.string().uuid('Chat ID must be a valid UUID'),
width: z.number().default(DEFAULT_SCREENSHOT_CONFIG.width).optional(),
height: z.number().default(DEFAULT_SCREENSHOT_CONFIG.height).optional(),
type: z.enum(['png', 'jpeg']).default(DEFAULT_SCREENSHOT_CONFIG.type).optional(),
})
.extend(BrowserParamsContextSchema.shape);
export type GetChatScreenshotHandlerArgs = z.infer<typeof GetChatScreenshotHandlerArgsSchema>;
export const getChatScreenshot = async (args: GetChatScreenshotHandlerArgs) => {
const { type = DEFAULT_SCREENSHOT_CONFIG.type } = args;
const { result: screenshotBuffer } = await browserLogin({
...args,
fullPath: createHrefFromLink({
to: '/screenshots/chats/$chatId/content' as const,
params: { chatId: args.chatId },
}),
callback: async ({ page }) => {
const screenshotBuffer = await page.screenshot({ type });
return screenshotBuffer;
},
callback: takeScreenshot,
});
return screenshotBuffer;

View File

@ -2,14 +2,12 @@ import { z } from 'zod';
import { BrowserParamsContextSchema, browserLogin } from './browser-login';
import { createHrefFromLink } from './create-href-from-link';
import { DEFAULT_SCREENSHOT_CONFIG } from './screenshot-config';
import { takeScreenshot } from './take-screenshot';
export const GetDashboardScreenshotHandlerArgsSchema = z
.object({
dashboardId: z.string().uuid('Dashboard ID must be a valid UUID'),
width: z.number().default(DEFAULT_SCREENSHOT_CONFIG.width).optional(),
height: z.number().default(DEFAULT_SCREENSHOT_CONFIG.height).optional(),
version_number: z.number().optional(),
type: z.enum(['png', 'jpeg']).default(DEFAULT_SCREENSHOT_CONFIG.type).optional(),
})
.extend(BrowserParamsContextSchema.shape);
@ -18,8 +16,6 @@ export type GetDashboardScreenshotHandlerArgs = z.infer<
>;
export const getDashboardScreenshot = async (args: GetDashboardScreenshotHandlerArgs) => {
const { type = DEFAULT_SCREENSHOT_CONFIG.type } = args;
const { result: screenshotBuffer } = await browserLogin({
...args,
fullPath: createHrefFromLink({
@ -29,10 +25,7 @@ export const getDashboardScreenshot = async (args: GetDashboardScreenshotHandler
version_number: args.version_number,
},
}),
callback: async ({ page }) => {
const screenshotBuffer = await page.screenshot({ type });
return screenshotBuffer;
},
callback: takeScreenshot,
});
return screenshotBuffer;

View File

@ -1,15 +1,12 @@
import { z } from 'zod';
import { BrowserParamsContextSchema, browserLogin } from './browser-login';
import { createHrefFromLink } from './create-href-from-link';
import { DEFAULT_SCREENSHOT_CONFIG } from './screenshot-config';
import { takeScreenshot } from './take-screenshot';
export const GetMetricScreenshotHandlerArgsSchema = z
.object({
metricId: z.string().uuid('Metric ID must be a valid UUID'),
width: z.number().default(DEFAULT_SCREENSHOT_CONFIG.width).optional(),
height: z.number().default(DEFAULT_SCREENSHOT_CONFIG.height).optional(),
version_number: z.number().optional(),
type: z.enum(['png', 'jpeg']).default(DEFAULT_SCREENSHOT_CONFIG.type).optional(),
})
.extend(BrowserParamsContextSchema.shape);
@ -18,8 +15,6 @@ export type GetMetricScreenshotHandlerArgs = z.infer<typeof GetMetricScreenshotH
export const getMetricScreenshot = async (
args: GetMetricScreenshotHandlerArgs
): Promise<Buffer<ArrayBufferLike>> => {
const { type = DEFAULT_SCREENSHOT_CONFIG.type } = args;
const { result: screenshotBuffer } = await browserLogin({
...args,
fullPath: createHrefFromLink({
@ -29,10 +24,7 @@ export const getMetricScreenshot = async (
version_number: args.version_number,
},
}),
callback: async ({ page }) => {
const screenshotBuffer = await page.screenshot({ type });
return screenshotBuffer;
},
callback: takeScreenshot,
});
return screenshotBuffer;

View File

@ -1,23 +1,18 @@
import { z } from 'zod';
import { BrowserParamsContextSchema, browserLogin } from './browser-login';
import { createHrefFromLink } from './create-href-from-link';
import { DEFAULT_SCREENSHOT_CONFIG } from './screenshot-config';
import { takeScreenshot } from './take-screenshot';
export const GetReportScreenshotHandlerArgsSchema = z
.object({
reportId: z.string().uuid('Report ID must be a valid UUID'),
width: z.number().default(DEFAULT_SCREENSHOT_CONFIG.width).optional(),
height: z.number().default(DEFAULT_SCREENSHOT_CONFIG.height).optional(),
version_number: z.number().optional(),
type: z.enum(['png', 'jpeg']).default(DEFAULT_SCREENSHOT_CONFIG.type).optional(),
})
.extend(BrowserParamsContextSchema.shape);
export type GetReportScreenshotHandlerArgs = z.infer<typeof GetReportScreenshotHandlerArgsSchema>;
export const getReportScreenshot = async (args: GetReportScreenshotHandlerArgs) => {
const { type = DEFAULT_SCREENSHOT_CONFIG.type } = args;
const { result: screenshotBuffer } = await browserLogin({
...args,
fullPath: createHrefFromLink({
@ -27,10 +22,7 @@ export const getReportScreenshot = async (args: GetReportScreenshotHandlerArgs)
version_number: args.version_number,
},
}),
callback: async ({ page }) => {
const screenshotBuffer = await page.screenshot({ type });
return screenshotBuffer;
},
callback: takeScreenshot,
});
return screenshotBuffer;

View File

@ -3,5 +3,6 @@ const multiplier = 2;
export const DEFAULT_SCREENSHOT_CONFIG = {
width: 400 * multiplier,
height: 240 * multiplier,
type: 'png' as const,
type: 'webp' as const,
deviceScaleFactor: 1.62,
};

View File

@ -0,0 +1,19 @@
import type { Page } from 'playwright';
import sharp from 'sharp';
export const takeScreenshot = async ({ page, type }: { page: Page; type: 'png' | 'webp' }) => {
const screenshotBuffer = await page.screenshot({ type: 'png' });
if (type === 'png') {
return await sharp(screenshotBuffer)
.png({
compressionLevel: 2,
quality: 100,
})
.toBuffer();
}
const compressed = await sharp(screenshotBuffer)
.webp({ nearLossless: true }) // Much smaller than PNG with same quality
.toBuffer();
return compressed;
};

View File

@ -15,12 +15,6 @@ export const GetChatScreenshotParamsSchema = z.object({
export type GetChatScreenshotParams = z.infer<typeof GetChatScreenshotParamsSchema>;
export const GetChatScreenshotQuerySchema = z
.object({
width: z.coerce.number().min(600).max(3840).default(600),
height: z.coerce.number().min(300).max(2160).default(338),
type: z.enum(['png', 'jpeg']).default('png'),
})
.merge(BaseScreenshotSearchSchema);
export const GetChatScreenshotQuerySchema = z.object({}).merge(BaseScreenshotSearchSchema);
export type GetChatScreenshotQuery = z.infer<typeof GetChatScreenshotQuerySchema>;

View File

@ -17,9 +17,6 @@ export type GetDashboardScreenshotParams = z.infer<typeof GetDashboardScreenshot
export const GetDashboardScreenshotQuerySchema = z
.object({
width: z.coerce.number().min(100).max(3840).default(800),
height: z.coerce.number().min(100).max(4160).default(450),
type: z.enum(['png', 'jpeg']).default('png'),
version_number: z.coerce.number().optional(),
})
.merge(BaseScreenshotSearchSchema);

View File

@ -8,9 +8,6 @@ export const GetMetricScreenshotParamsSchema = z.object({
export const GetMetricScreenshotQuerySchema = z
.object({
version_number: z.coerce.number().min(1).optional(),
width: z.coerce.number().min(100).max(3840).default(800),
height: z.coerce.number().min(100).max(2160).default(450),
type: z.enum(['png', 'jpeg']).default('png').optional(),
})
.merge(BaseScreenshotSearchSchema);

View File

@ -17,9 +17,6 @@ export type GetReportScreenshotParams = z.infer<typeof GetReportScreenshotParams
export const GetReportScreenshotQuerySchema = z
.object({
width: z.coerce.number().min(100).max(3840).default(800),
height: z.coerce.number().min(100).max(2160).default(450),
type: z.enum(['png', 'jpeg']).default('png'),
version_number: z.coerce.number().optional(),
})
.merge(BaseScreenshotSearchSchema);

View File

@ -1359,6 +1359,9 @@ importers:
playwright:
specifier: ^1.55.1
version: 1.55.1
sharp:
specifier: ^0.34.4
version: 0.34.4
zod:
specifier: 'catalog:'
version: 3.25.76
@ -2394,6 +2397,9 @@ packages:
react:
optional: true
'@emnapi/runtime@1.5.0':
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
'@emoji-mart/data@1.2.1':
resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==}
@ -3124,6 +3130,146 @@ packages:
'@iarna/toml@2.2.5':
resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
'@img/colour@1.0.0':
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.34.4':
resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.4':
resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.2.3':
resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.3':
resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.2.3':
resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.3':
resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.3':
resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.3':
resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.3':
resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.3':
resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.3':
resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.4':
resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.4':
resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.4':
resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.4':
resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.4':
resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.4':
resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.4':
resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.4':
resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.34.4':
resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.34.4':
resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.34.4':
resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@inquirer/ansi@1.0.0':
resolution: {integrity: sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==}
engines: {node: '>=18'}
@ -8012,6 +8158,10 @@ packages:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
@ -11752,6 +11902,10 @@ packages:
resolution: {integrity: sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==}
engines: {node: '>=0.10.0'}
sharp@0.34.4:
resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -15099,6 +15253,11 @@ snapshots:
optionalDependencies:
react: 19.1.1
'@emnapi/runtime@1.5.0':
dependencies:
tslib: 2.8.1
optional: true
'@emoji-mart/data@1.2.1': {}
'@epic-web/invariant@1.0.0': {}
@ -15568,6 +15727,94 @@ snapshots:
'@iarna/toml@2.2.5': {}
'@img/colour@1.0.0': {}
'@img/sharp-darwin-arm64@0.34.4':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.3
optional: true
'@img/sharp-darwin-x64@0.34.4':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.3
optional: true
'@img/sharp-libvips-darwin-arm64@1.2.3':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.3':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.3':
optional: true
'@img/sharp-libvips-linux-arm@1.2.3':
optional: true
'@img/sharp-libvips-linux-ppc64@1.2.3':
optional: true
'@img/sharp-libvips-linux-s390x@1.2.3':
optional: true
'@img/sharp-libvips-linux-x64@1.2.3':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.3':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.3':
optional: true
'@img/sharp-linux-arm64@0.34.4':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.3
optional: true
'@img/sharp-linux-arm@0.34.4':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.3
optional: true
'@img/sharp-linux-ppc64@0.34.4':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.3
optional: true
'@img/sharp-linux-s390x@0.34.4':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.3
optional: true
'@img/sharp-linux-x64@0.34.4':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.3
optional: true
'@img/sharp-linuxmusl-arm64@0.34.4':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.3
optional: true
'@img/sharp-linuxmusl-x64@0.34.4':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.3
optional: true
'@img/sharp-wasm32@0.34.4':
dependencies:
'@emnapi/runtime': 1.5.0
optional: true
'@img/sharp-win32-arm64@0.34.4':
optional: true
'@img/sharp-win32-ia32@0.34.4':
optional: true
'@img/sharp-win32-x64@0.34.4':
optional: true
'@inquirer/ansi@1.0.0': {}
'@inquirer/checkbox@4.2.4(@types/node@24.3.1)':
@ -19970,11 +20217,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/browser@3.2.4(msw@2.11.4(@types/node@22.18.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.4(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4)':
'@vitest/browser@3.2.4(msw@2.11.4(@types/node@22.18.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4)':
dependencies:
'@testing-library/dom': 10.4.1
'@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1)
'@vitest/mocker': 3.2.4(msw@2.11.4(@types/node@22.18.1)(typescript@5.9.2))(vite@7.1.4(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))
'@vitest/mocker': 3.2.4(msw@2.11.4(@types/node@22.18.1)(typescript@5.9.2))(vite@7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))
'@vitest/utils': 3.2.4
magic-string: 0.30.17
sirv: 3.0.1
@ -20024,7 +20271,7 @@ snapshots:
std-env: 3.9.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.4(@types/node@24.3.1)(typescript@5.9.2))(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)
vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.11.4(@types/node@24.3.1)(typescript@5.9.2))(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)
optionalDependencies:
'@vitest/browser': 3.2.4(msw@2.11.4(@types/node@24.3.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.4(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4)
transitivePeerDependencies:
@ -20047,6 +20294,16 @@ snapshots:
msw: 2.11.4(@types/node@22.18.1)(typescript@5.9.2)
vite: 7.1.4(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)
'@vitest/mocker@3.2.4(msw@2.11.4(@types/node@22.18.1)(typescript@5.9.2))(vite@7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
msw: 2.11.4(@types/node@22.18.1)(typescript@5.9.2)
vite: 7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)
optional: true
'@vitest/mocker@3.2.4(msw@2.11.4(@types/node@24.3.1)(typescript@5.9.2))(vite@7.1.4(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 3.2.4
@ -20085,7 +20342,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.14
tinyrainbow: 2.0.0
vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.4(@types/node@22.18.1)(typescript@5.9.2))(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)
vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.4(@types/node@24.3.1)(typescript@5.9.2))(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)
'@vitest/utils@3.2.4':
dependencies:
@ -21288,6 +21545,8 @@ snapshots:
detect-libc@2.0.4: {}
detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
devalue@5.3.2: {}
@ -25990,6 +26249,35 @@ snapshots:
lazy-cache: 0.2.7
mixin-object: 2.0.1
sharp@0.34.4:
dependencies:
'@img/colour': 1.0.0
detect-libc: 2.1.2
semver: 7.7.2
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.4
'@img/sharp-darwin-x64': 0.34.4
'@img/sharp-libvips-darwin-arm64': 1.2.3
'@img/sharp-libvips-darwin-x64': 1.2.3
'@img/sharp-libvips-linux-arm': 1.2.3
'@img/sharp-libvips-linux-arm64': 1.2.3
'@img/sharp-libvips-linux-ppc64': 1.2.3
'@img/sharp-libvips-linux-s390x': 1.2.3
'@img/sharp-libvips-linux-x64': 1.2.3
'@img/sharp-libvips-linuxmusl-arm64': 1.2.3
'@img/sharp-libvips-linuxmusl-x64': 1.2.3
'@img/sharp-linux-arm': 0.34.4
'@img/sharp-linux-arm64': 0.34.4
'@img/sharp-linux-ppc64': 0.34.4
'@img/sharp-linux-s390x': 0.34.4
'@img/sharp-linux-x64': 0.34.4
'@img/sharp-linuxmusl-arm64': 0.34.4
'@img/sharp-linuxmusl-x64': 0.34.4
'@img/sharp-wasm32': 0.34.4
'@img/sharp-win32-arm64': 0.34.4
'@img/sharp-win32-ia32': 0.34.4
'@img/sharp-win32-x64': 0.34.4
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@ -27344,6 +27632,25 @@ snapshots:
tsx: 4.20.5
yaml: 2.8.1
vite@7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1):
dependencies:
esbuild: 0.25.9
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.50.0
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.18.1
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.30.1
sass: 1.93.2
terser: 5.43.1
tsx: 4.20.5
yaml: 2.8.1
optional: true
vite@7.1.9(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1):
dependencies:
esbuild: 0.25.9
@ -27405,7 +27712,7 @@ snapshots:
'@edge-runtime/vm': 3.2.0
'@types/debug': 4.1.12
'@types/node': 22.18.1
'@vitest/browser': 3.2.4(msw@2.11.4(@types/node@22.18.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.4(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4)
'@vitest/browser': 3.2.4(msw@2.11.4(@types/node@22.18.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4)
'@vitest/ui': 3.2.4(vitest@3.2.4)
jsdom: 27.0.0(postcss@8.5.6)
transitivePeerDependencies: