diff --git a/apps/ssr/src/app/og/[photoId]/route.tsx b/apps/ssr/src/app/og/[photoId]/route.tsx index 9bbf9ba7..2ca03fb8 100644 --- a/apps/ssr/src/app/og/[photoId]/route.tsx +++ b/apps/ssr/src/app/og/[photoId]/route.tsx @@ -1,826 +1,68 @@ -import { siteConfig } from '@config' -import { ImageResponse } from 'next/og' import type { NextRequest } from 'next/server' -import { photoLoader } from '~/lib/photo-loader' +const CORE_API_BASE = + process.env.CORE_API_URL ?? + process.env.NEXT_PUBLIC_CORE_API_URL ?? + process.env.API_BASE_URL ?? + 'http://localhost:3000' -import geistFont from './Geist-Medium.ttf' -import Sans from './PingFangSC.ttf' +const FORWARDED_HEADER_KEYS = [ + 'cookie', + 'authorization', + 'x-tenant-id', + 'x-tenant-slug', + 'x-forwarded-host', + 'x-forwarded-proto', + 'host', +] + +function buildBackendUrl(photoId: string): string { + const base = CORE_API_BASE.endsWith('/') ? CORE_API_BASE.slice(0, -1) : CORE_API_BASE + return `${base}/og/${encodeURIComponent(photoId)}` +} + +function buildForwardHeaders(request: NextRequest): Headers { + const headers = new Headers() + + for (const key of FORWARDED_HEADER_KEYS) { + const value = request.headers.get(key) + if (value) { + headers.set(key, value) + } + } + + const hostHeader = request.headers.get('host') + if (!headers.has('x-forwarded-host') && hostHeader) { + headers.set('x-forwarded-host', hostHeader) + } + + if (!headers.has('x-forwarded-proto')) { + headers.set('x-forwarded-proto', request.nextUrl.protocol.replace(':', '')) + } + + headers.set('accept', 'image/png,image/*;q=0.9,*/*;q=0.8') + return headers +} + +export const revalidate = 0 export const GET = async (request: NextRequest, { params }: { params: Promise<{ photoId: string }> }) => { const { photoId } = await params + const targetUrl = buildBackendUrl(photoId) - const photo = photoLoader.getPhoto(photoId) - if (!photo) { - return new Response('Photo not found', { status: 404 }) - } + const response = await fetch(targetUrl, { + headers: buildForwardHeaders(request), + }) - try { - // 格式化拍摄时间 - const dateTaken = photo.exif?.DateTimeOriginal || photo.lastModified - const formattedDate = dateTaken ? new Date(dateTaken).toLocaleDateString('en-US') : '' - - // 处理标签 - const tags = photo.tags?.slice(0, 3).join(' • ') || '' - // Format EXIF information - const formatExifInfo = () => { - if (!photo.exif) return null - - const info = { - focalLength: photo.exif.FocalLengthIn35mmFormat || photo.exif.FocalLength, - aperture: photo.exif.FNumber ? `f/${photo.exif.FNumber}` : null, - iso: photo.exif.ISO || null, - shutterSpeed: `${photo.exif.ExposureTime}s`, - camera: photo.exif.Make && photo.exif.Model ? `${photo.exif.Make} ${photo.exif.Model}` : null, - } - - return info - } - - const exifInfo = formatExifInfo() - const thumbnailBuffer = await Promise.any([ - fetch(`http://localhost:13333${photo.thumbnailUrl.replace('.webp', '.jpg')}`).then((res) => res.arrayBuffer()), - process.env.NEXT_PUBLIC_APP_URL - ? fetch(`http://${process.env.NEXT_PUBLIC_APP_URL}${photo.thumbnailUrl.replace('.webp', '.jpg')}`).then((res) => - res.arrayBuffer(), - ) - : Promise.reject(), - fetch(`http://${request.nextUrl.host}${photo.thumbnailUrl.replace('.webp', '.jpg')}`).then((res) => - res.arrayBuffer(), - ), - ]) - - // 计算图片显示尺寸以保持原始比例 - const imageWidth = photo.width || 1 - const imageHeight = photo.height || 1 - const aspectRatio = imageWidth / imageHeight - - // 胶片框的最大尺寸 - const maxFrameWidth = 500 - const maxFrameHeight = 420 - - // 计算胶片框尺寸(保持图片比例) - let frameWidth = maxFrameWidth - let frameHeight = maxFrameHeight - - if (aspectRatio > maxFrameWidth / maxFrameHeight) { - // 图片较宽,以宽度为准 - frameHeight = maxFrameWidth / aspectRatio - } else { - // 图片较高,以高度为准 - frameWidth = maxFrameHeight * aspectRatio - } - - // 图片区域尺寸(减去胶片边框) - const imageAreaWidth = frameWidth - 70 - const imageAreaHeight = frameHeight - 70 - - // 计算实际图片显示尺寸 - let displayWidth = imageAreaWidth - let displayHeight = imageAreaHeight - - if (aspectRatio > imageAreaWidth / imageAreaHeight) { - // 图片较宽,以宽度为准 - displayHeight = imageAreaWidth / aspectRatio - } else { - // 图片较高,以高度为准 - displayWidth = imageAreaHeight * aspectRatio - } - - return new ImageResponse( - ( -
- {/* 摄影师风格的网格背景 */} -
- - {/* 主光源效果 - 左上角 */} -
- - {/* 副光源效果 - 右下角 */} -
- - {/* 摄影工作室的聚光灯效果 */} -
- - {/* 胶片装饰元素 */} -
- {/* 胶片孔 */} -
-
-
-
-
-
-
- - {/* 几何装饰线条 - 多个层次 */} -
- -
- -
- - {/* 光圈装饰 */} -
- {/* 内圈 */} -
-
-
-
- - {/* 主要内容区域 */} -
- {/* 标题 */} -

- {photo.title || 'Untitled Photo'} -

- - {/* 描述 */} -

- {photo.description || siteConfig.name || siteConfig.title} -

- - {/* 标签 */} - {tags && ( -
- {photo.tags?.slice(0, 3).map((tag, index) => ( -
- #{tag} -
- ))} -
- )} -
- - {/* 照片缩略图 - 胶片风格 */} - {photo.thumbnailUrl && ( -
- {/* 胶片左边的孔洞 */} -
- {/* 胶片孔洞 - 更柔和的边缘 */} -
-
-
-
-
-
-
-
- - {/* 胶片右边的孔洞 */} -
- {/* 胶片孔洞 - 更柔和的边缘 */} -
-
-
-
-
-
-
-
- - {/* 胶片中间的照片区域 */} -
-
- -
- - {/* 胶片光泽效果 - 更柔和 */} -
-
- - {/* 胶片顶部和底部的纹理 - 更细腻 */} -
-
- - {/* 胶片编号 - 更自然的位置 */} -
- {photoId.slice(-4).toUpperCase()} -
- - {/* 胶片质感的整体覆盖层 */} -
-
- )} - - {/* 底部信息 */} -
- {/* 拍摄时间 */} - {formattedDate && ( -
- 📸 {formattedDate} -
- )} - {/* 相机信息 */} - {exifInfo?.camera && ( -
- 📷 {exifInfo.camera} -
- )} - {/* EXIF 信息 */} - {exifInfo && (exifInfo.aperture || exifInfo.shutterSpeed || exifInfo.iso || exifInfo.focalLength) && ( -
- {exifInfo.aperture && ( -
- ⚫ {exifInfo.aperture} -
- )} - - {exifInfo.shutterSpeed && ( -
- ⏱️ {exifInfo.shutterSpeed} -
- )} - - {exifInfo.iso && ( -
- 📊 ISO {exifInfo.iso} -
- )} - - {exifInfo.focalLength && ( -
- 🔍 {exifInfo.focalLength} -
- )} -
- )} -
-
- ), - { - width: 1200, - height: 628, - emoji: 'noto', - fonts: [ - { - name: 'Geist', - data: geistFont, - style: 'normal', - weight: 400, - }, - { - name: 'SF Pro Display', - data: Sans, - style: 'normal', - weight: 400, - }, - ], - headers: { - // Cache 1 years - 'Cache-Control': 'public, max-age=31536000, stale-while-revalidate=31536000', - 'Cloudflare-CDN-Cache-Control': 'public, max-age=31536000, stale-while-revalidate=31536000', - }, - }, - ) - } catch (error) { - console.error('Failed to generate OG image:', error) - return new Response(`Failed to generate image, ${error.message}`, { - status: 500, + if (!response.ok) { + return new Response(await response.text(), { + status: response.status, + headers: response.headers, }) } + + return new Response(response.body, { + status: response.status, + headers: response.headers, + }) } diff --git a/apps/web/plugins/vite/feed-sitemap.ts b/apps/web/plugins/vite/feed-sitemap.ts index 892db709..706b6be3 100644 --- a/apps/web/plugins/vite/feed-sitemap.ts +++ b/apps/web/plugins/vite/feed-sitemap.ts @@ -1,12 +1,14 @@ import { readFileSync } from 'node:fs' import type { PhotoManifestItem } from '@afilmory/builder' -import { generateRSSFeed } from '@afilmory/utils' +import { tsImport } from 'tsx/esm/api' import type { Plugin } from 'vite' import type { SiteConfig } from '../../../../site.config' import { MANIFEST_PATH } from './__internal__/constants' +const { generateRSSFeed } = await tsImport('@afilmory/utils', import.meta.url) + export function createFeedSitemapPlugin(siteConfig: SiteConfig): Plugin { return { name: 'feed-sitemap-generator', diff --git a/apps/web/src/components/ui/photo-viewer/ExifPanel.tsx b/apps/web/src/components/ui/photo-viewer/ExifPanel.tsx index bffccdb2..5174edc6 100644 --- a/apps/web/src/components/ui/photo-viewer/ExifPanel.tsx +++ b/apps/web/src/components/ui/photo-viewer/ExifPanel.tsx @@ -72,7 +72,7 @@ export const ExifPanel: FC<{ style={{ pointerEvents: visible ? 'auto' : 'none', backgroundImage: - 'linear-gradient(to bottom right, color-mix(in srgb, var(--color-background) 98%, transparent), color-mix(in srgb, var(--color-background) 95%, transparent))', + 'linear-gradient(to bottom right, rgba(var(--color-materialMedium)), rgba(var(--color-materialThick)), transparent)', boxShadow: '0 8px 32px color-mix(in srgb, var(--color-accent) 8%, transparent), 0 4px 16px color-mix(in srgb, var(--color-accent) 6%, transparent), 0 2px 8px rgba(0, 0, 0, 0.1)', }} @@ -101,7 +101,7 @@ export const ExifPanel: FC<{
{/* 基本信息和标签 - 合并到一个 section */} diff --git a/apps/web/src/components/ui/photo-viewer/GalleryThumbnail.tsx b/apps/web/src/components/ui/photo-viewer/GalleryThumbnail.tsx index e34dfa37..e3533571 100644 --- a/apps/web/src/components/ui/photo-viewer/GalleryThumbnail.tsx +++ b/apps/web/src/components/ui/photo-viewer/GalleryThumbnail.tsx @@ -93,7 +93,7 @@ export const GalleryThumbnail: FC<{ return ( { + const svg = await satori(, { + width: 1200, + height: 628, + fonts, + embedFont: true, + }) + + const svgInput = typeof svg === 'string' ? svg : Buffer.from(svg) + const renderer = new Resvg(svgInput, { + fitTo: { mode: 'width', value: 1200 }, + background: 'rgba(0,0,0,0)', + }) + + return renderer.render().asPng() +} diff --git a/be/apps/core/src/modules/og/og.service.ts b/be/apps/core/src/modules/og/og.service.ts new file mode 100644 index 00000000..9b474ba6 --- /dev/null +++ b/be/apps/core/src/modules/og/og.service.ts @@ -0,0 +1,362 @@ +import { readFile, stat } from 'node:fs/promises' +import { resolve } from 'node:path' + +import type { PhotoManifestItem } from '@afilmory/builder' +import { BizException, ErrorCode } from 'core/errors' +import type { Context } from 'hono' +import type { SatoriOptions } from 'satori' +import { injectable } from 'tsyringe' + +import { ManifestService } from '../manifest/manifest.service' +import { SiteSettingService } from '../site-setting/site-setting.service' +import GeistMedium from './assets/Geist-Medium.ttf.ts' +import PingFangSC from './assets/PingFangSC.ttf.ts' +import { renderOgImage } from './og.renderer' +import type { ExifInfo, FrameDimensions } from './og.template' + +const CACHE_CONTROL = 'public, max-age=31536000, stale-while-revalidate=31536000' +const LOCAL_THUMBNAIL_ROOT_CANDIDATES = [ + resolve(process.cwd(), 'dist/static/web'), + resolve(process.cwd(), '../dist/static/web'), + resolve(process.cwd(), '../../dist/static/web'), + resolve(process.cwd(), 'static/web'), + resolve(process.cwd(), '../static/web'), + resolve(process.cwd(), '../../static/web'), + resolve(process.cwd(), 'apps/web/dist'), + resolve(process.cwd(), '../apps/web/dist'), + resolve(process.cwd(), '../../apps/web/dist'), + resolve(process.cwd(), 'apps/web/public'), + resolve(process.cwd(), '../apps/web/public'), + resolve(process.cwd(), '../../apps/web/public'), +] + +interface ThumbnailCandidateResult { + buffer: Buffer + contentType: string +} + +@injectable() +export class OgService { + private fontConfig: SatoriOptions['fonts'] | null = null + private localThumbnailRoots: string[] | null = null + + constructor( + private readonly manifestService: ManifestService, + private readonly siteSettingService: SiteSettingService, + ) {} + + async render(context: Context, photoId: string): Promise { + const manifest = await this.manifestService.getManifest() + const photo = manifest.data.find((entry) => entry.id === photoId) + if (!photo) { + throw new BizException(ErrorCode.COMMON_NOT_FOUND, { message: 'Photo not found' }) + } + + const siteConfig = await this.siteSettingService.getSiteConfig() + const formattedDate = this.formatDate(photo.exif?.DateTimeOriginal ?? photo.lastModified) + const exifInfo = this.buildExifInfo(photo) + const frame = this.computeFrameDimensions(photo) + const tags = (photo.tags ?? []).slice(0, 3) + const thumbnailSrc = await this.resolveThumbnailSrc(context, photo) + + const png = await renderOgImage({ + template: { + photoTitle: photo.title || photo.id || 'Untitled Photo', + photoDescription: photo.description || siteConfig.name || siteConfig.title || '', + tags, + formattedDate, + exifInfo, + thumbnailSrc, + frame, + photoId: photo.id, + }, + fonts: await this.getFontConfig(), + }) + const headers = new Headers({ + 'content-type': 'image/png', + 'cache-control': CACHE_CONTROL, + 'cloudflare-cdn-cache-control': CACHE_CONTROL, + }) + + const body = this.toArrayBuffer(png) + + return new Response(body, { status: 200, headers }) + } + + private async getFontConfig(): Promise { + if (this.fontConfig) { + return this.fontConfig + } + + this.fontConfig = [ + { + name: 'Geist', + data: this.toArrayBuffer(GeistMedium), + style: 'normal', + weight: 400, + }, + { + name: 'SF Pro Display', + data: this.toArrayBuffer(PingFangSC), + style: 'normal', + weight: 400, + }, + ] + + return this.fontConfig + } + + private toArrayBuffer(source: ArrayBufferView): ArrayBuffer { + const { buffer, byteOffset, byteLength } = source + + if (buffer instanceof ArrayBuffer) { + return buffer.slice(byteOffset, byteOffset + byteLength) + } + + const copy = new ArrayBuffer(byteLength) + const view = new Uint8Array(buffer, byteOffset, byteLength) + new Uint8Array(copy).set(view) + + return copy + } + + private formatDate(input?: string | null): string | undefined { + if (!input) { + return undefined + } + + const timestamp = Date.parse(input) + if (Number.isNaN(timestamp)) { + return undefined + } + + return new Date(timestamp).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }) + } + + private buildExifInfo(photo: PhotoManifestItem): ExifInfo | null { + const { exif } = photo + if (!exif) { + return null + } + + const focalLength = exif.FocalLengthIn35mmFormat || exif.FocalLength + const aperture = exif.FNumber ? `f/${exif.FNumber}` : null + const iso = exif.ISO ?? null + const shutterSpeed = exif.ExposureTime ? `${exif.ExposureTime}s` : null + const camera = + exif.Make && exif.Model ? `${exif.Make.trim()} ${exif.Model.trim()}`.trim() : (exif.Model ?? exif.Make ?? null) + + if (!focalLength && !aperture && !iso && !shutterSpeed && !camera) { + return null + } + + return { + focalLength: focalLength ?? null, + aperture, + iso, + shutterSpeed, + camera, + } + } + + private computeFrameDimensions(photo: PhotoManifestItem): FrameDimensions { + const imageWidth = photo.width || 1 + const imageHeight = photo.height || 1 + const aspectRatio = imageWidth / imageHeight + + const maxFrameWidth = 500 + const maxFrameHeight = 420 + let frameWidth = maxFrameWidth + let frameHeight = maxFrameHeight + + if (aspectRatio > maxFrameWidth / maxFrameHeight) { + frameHeight = maxFrameWidth / aspectRatio + } else { + frameWidth = maxFrameHeight * aspectRatio + } + + const imageAreaWidth = frameWidth - 70 + const imageAreaHeight = frameHeight - 70 + + let displayWidth = imageAreaWidth + let displayHeight = imageAreaHeight + + if (aspectRatio > imageAreaWidth / imageAreaHeight) { + displayHeight = imageAreaWidth / aspectRatio + } else { + displayWidth = imageAreaHeight * aspectRatio + } + + return { + frameWidth, + frameHeight, + imageAreaWidth, + imageAreaHeight, + displayWidth, + displayHeight, + } + } + + private async resolveThumbnailSrc(context: Context, photo: PhotoManifestItem): Promise { + const normalized = this.normalizeThumbnailPath(photo.thumbnailUrl) + if (!normalized) { + return null + } + + const fetched = await this.fetchThumbnailBuffer(context, normalized) + if (!fetched) { + return null + } + + return this.bufferToDataUrl(fetched.buffer, fetched.contentType) + } + + private normalizeThumbnailPath(value?: string | null): string | null { + if (!value) { + return null + } + + const replaced = value.replace(/\.webp$/i, '.jpg') + return replaced + } + + private async fetchThumbnailBuffer( + context: Context, + thumbnailPath: string, + ): Promise { + const requests = this.buildThumbnailUrlCandidates(context, thumbnailPath) + for (const candidate of requests) { + const fetched = await this.tryFetchUrl(candidate) + if (fetched) { + return fetched + } + } + + const local = await this.tryReadLocalThumbnail(thumbnailPath) + if (local) { + return { + buffer: local, + contentType: 'image/jpeg', + } + } + + return null + } + + private async tryFetchUrl(url: string): Promise { + try { + const response = await fetch(url) + if (!response.ok) { + return null + } + const arrayBuffer = await response.arrayBuffer() + const contentType = response.headers.get('content-type') ?? 'image/jpeg' + return { + buffer: Buffer.from(arrayBuffer), + contentType, + } + } catch { + return null + } + } + + private async tryReadLocalThumbnail(thumbnailPath: string): Promise { + const roots = await this.getLocalThumbnailRoots() + if (roots.length === 0) { + return null + } + + const normalizedPath = thumbnailPath.startsWith('/') ? thumbnailPath.slice(1) : thumbnailPath + const candidates = [normalizedPath] + if (!normalizedPath.startsWith('static/web/')) { + candidates.push(`static/web/${normalizedPath}`) + } + + for (const root of roots) { + for (const candidate of candidates) { + try { + const absolute = resolve(root, candidate) + return await readFile(absolute) + } catch { + continue + } + } + } + + return null + } + + private async getLocalThumbnailRoots(): Promise { + if (this.localThumbnailRoots) { + return this.localThumbnailRoots + } + + const resolved: string[] = [] + for (const candidate of LOCAL_THUMBNAIL_ROOT_CANDIDATES) { + try { + const stats = await stat(candidate) + if (stats.isDirectory()) { + resolved.push(candidate) + } + } catch { + continue + } + } + + this.localThumbnailRoots = resolved + return resolved + } + + private buildThumbnailUrlCandidates(context: Context, thumbnailPath: string): string[] { + const result: string[] = [] + const externalOverride = process.env.OG_THUMBNAIL_ORIGIN?.trim() + const normalizedPath = thumbnailPath.startsWith('/') ? thumbnailPath : `/${thumbnailPath}` + + if (thumbnailPath.startsWith('http://') || thumbnailPath.startsWith('https://')) { + result.push(thumbnailPath) + } else { + const base = this.resolveBaseUrl(context) + if (base) { + result.push(new URL(normalizedPath, base).toString()) + if (!normalizedPath.startsWith('/static/web/')) { + result.push(new URL(`/static/web${normalizedPath}`, base).toString()) + } + } + + if (externalOverride) { + result.push(`${externalOverride.replace(/\/+$/, '')}${normalizedPath}`) + } + } + + return Array.from(new Set(result)) + } + + private resolveBaseUrl(context: Context): URL | null { + const forwardedHost = context.req.header('x-forwarded-host') + const forwardedProto = context.req.header('x-forwarded-proto') + const host = forwardedHost ?? context.req.header('host') + + if (host) { + const protocol = forwardedProto ?? (host.includes('localhost') ? 'http' : 'https') + try { + return new URL(`${protocol}://${host}`) + } catch { + return null + } + } + + try { + return new URL(context.req.url) + } catch { + return null + } + } + + private bufferToDataUrl(buffer: Buffer, contentType: string): string { + return `data:${contentType};base64,${buffer.toString('base64')}` + } +} diff --git a/be/apps/core/src/modules/og/og.template.tsx b/be/apps/core/src/modules/og/og.template.tsx new file mode 100644 index 00000000..799168ad --- /dev/null +++ b/be/apps/core/src/modules/og/og.template.tsx @@ -0,0 +1,595 @@ +/** @jsxImportSource hono/jsx */ +import type { JSX } from 'hono/jsx' + +export interface FrameDimensions { + frameWidth: number + frameHeight: number + imageAreaWidth: number + imageAreaHeight: number + displayWidth: number + displayHeight: number +} + +export interface ExifInfo { + focalLength?: string | null + aperture?: string | null + iso?: string | number | null + shutterSpeed?: string | null + camera?: string | null +} + +export interface OgTemplateProps { + photoTitle: string + photoDescription: string + tags: string[] + formattedDate?: string + exifInfo?: ExifInfo | null + thumbnailSrc?: string | null + frame: FrameDimensions + photoId: string +} + +export function OgTemplate({ + photoTitle, + photoDescription, + tags, + formattedDate, + exifInfo, + thumbnailSrc, + frame, + photoId, +}: OgTemplateProps): JSX.Element { + return ( +
+
+ +
+ +
+ +
+ +
+ {Array.from({ length: 6 }).map((_value, index) => ( +
+ ))} +
+ +
+ +
+ +
+ +
+
+
+
+
+ +
+

+ {photoTitle || 'Untitled Photo'} +

+ +

+ {photoDescription} +

+ + {tags.length > 0 && ( +
+ {tags.map((tag) => ( +
+ #{tag} +
+ ))} +
+ )} +
+ + {thumbnailSrc && ( +
+
+ {Array.from({ length: 7 }).map((_value, index) => ( +
+ ))} +
+ +
+ {Array.from({ length: 7 }).map((_value, index) => ( +
+ ))} +
+ +
+
+ +
+ +
+
+ +
+
+ +
+ {photoId} +
+ +
+ FILM 400 | STUDIO CUT +
+ +
+
+ )} + +
+ {formattedDate && ( +
+ 📸 {formattedDate} +
+ )} + + {exifInfo?.camera && ( +
+ 📷 {exifInfo.camera} +
+ )} + + {exifInfo && (exifInfo.aperture || exifInfo.shutterSpeed || exifInfo.iso || exifInfo.focalLength) && ( +
+ {exifInfo.aperture && ( +
+ ⚫ {exifInfo.aperture} +
+ )} + + {exifInfo.shutterSpeed && ( +
+ ⏱️ {exifInfo.shutterSpeed} +
+ )} + + {exifInfo.iso && ( +
+ 📊 ISO {exifInfo.iso} +
+ )} + + {exifInfo.focalLength && ( +
+ 🔍 {exifInfo.focalLength} +
+ )} +
+ )} +
+
+ ) +} diff --git a/be/apps/core/tsconfig.json b/be/apps/core/tsconfig.json index 97e362c1..f54db48e 100644 --- a/be/apps/core/tsconfig.json +++ b/be/apps/core/tsconfig.json @@ -29,7 +29,9 @@ "sourceMap": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "skipLibCheck": true + "skipLibCheck": true, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" }, "include": ["src/**/*", "*.d.ts"], "exclude": [] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3af5601a..ceb23fb8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -604,6 +604,9 @@ importers: '@hono/node-server': specifier: ^1.19.6 version: 1.19.6(hono@4.10.4) + '@resvg/resvg-js': + specifier: 2.6.2 + version: 2.6.2 better-auth: specifier: 1.3.34 version: 1.3.34(next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -628,6 +631,9 @@ importers: reflect-metadata: specifier: 0.2.2 version: 0.2.2 + satori: + specifier: 0.18.3 + version: 0.18.3 tsyringe: specifier: 4.10.0 version: 4.10.0 @@ -1320,6 +1326,9 @@ importers: packages/utils: dependencies: + '@afilmory/builder': + specifier: workspace:* + version: link:../builder clsx: specifier: ^2.1.1 version: 2.1.1 @@ -4400,6 +4409,82 @@ packages: peerDependencies: react: '>=18.2.0' + '@resvg/resvg-js-android-arm-eabi@2.6.2': + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@resvg/resvg-js-android-arm64@2.6.2': + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@resvg/resvg-js-darwin-arm64@2.6.2': + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@resvg/resvg-js-darwin-x64@2.6.2': + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@resvg/resvg-js@2.6.2': + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} + engines: {node: '>= 10'} + '@reteps/dockerfmt@0.3.6': resolution: {integrity: sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg==} engines: {node: ^v12.20.0 || ^14.13.0 || >=16.0.0} @@ -4714,6 +4799,11 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@shuding/opentype.js@1.4.0-beta.0': + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + '@simplewebauthn/browser@13.2.2': resolution: {integrity: sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==} @@ -6076,6 +6166,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + baseline-browser-mapping@2.8.18: resolution: {integrity: sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==} hasBin: true @@ -6207,6 +6301,9 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + caniuse-lite@1.0.30001751: resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} @@ -6504,6 +6601,20 @@ packages: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} + css-background-parser@0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + + css-box-shadow@1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + + css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + + css-gradient-parser@0.0.17: + resolution: {integrity: sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==} + engines: {node: '>=16'} + css-in-js-utils@3.1.0: resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} @@ -6513,6 +6624,9 @@ packages: css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + css-tree@1.1.3: resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} engines: {node: '>=8.0.0'} @@ -6975,6 +7089,10 @@ packages: emoji-mart@5.6.0: resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} + emoji-regex-xs@2.0.1: + resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} + engines: {node: '>=10.0.0'} + emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -7076,6 +7194,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -7457,6 +7578,9 @@ packages: picomatch: optional: true + fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -7821,6 +7945,10 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -8407,6 +8535,9 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -9027,6 +9158,9 @@ packages: package-manager-detector@1.5.0: resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} @@ -9041,6 +9175,9 @@ packages: resolution: {integrity: sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==} engines: {node: '>=0.10.0'} + parse-css-color@0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -10213,6 +10350,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + satori@0.18.3: + resolution: {integrity: sha512-T3DzWNmnrfVmk2gCIlAxLRLbGkfp3K7TyRva+Byyojqu83BNvnMeqVeYRdmUw4TKCsyH4RiQ/KuF/I4yEzgR5A==} + engines: {node: '>=16'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -10956,6 +11097,9 @@ packages: resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} engines: {node: '>=4'} + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -11507,6 +11651,9 @@ packages: resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} engines: {node: '>=18'} + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zod-validation-error@3.5.3: resolution: {integrity: sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==} engines: {node: '>=18.0.0'} @@ -15464,6 +15611,57 @@ snapshots: dependencies: react: 19.2.0 + '@resvg/resvg-js-android-arm-eabi@2.6.2': + optional: true + + '@resvg/resvg-js-android-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-x64@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js@2.6.2': + optionalDependencies: + '@resvg/resvg-js-android-arm-eabi': 2.6.2 + '@resvg/resvg-js-android-arm64': 2.6.2 + '@resvg/resvg-js-darwin-arm64': 2.6.2 + '@resvg/resvg-js-darwin-x64': 2.6.2 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 + '@reteps/dockerfmt@0.3.6': {} '@rolldown/binding-android-arm64@1.0.0-beta.45': @@ -15742,6 +15940,11 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@shuding/opentype.js@1.4.0-beta.0': + dependencies: + fflate: 0.7.4 + string.prototype.codepointat: 0.2.1 + '@simplewebauthn/browser@13.2.2': {} '@simplewebauthn/server@13.2.2': @@ -17394,6 +17597,8 @@ snapshots: balanced-match@1.0.2: {} + base64-js@0.0.8: {} + baseline-browser-mapping@2.8.18: {} batch-cluster@15.0.1: {} @@ -17530,6 +17735,8 @@ snapshots: camelcase-css@2.0.1: {} + camelize@1.0.1: {} + caniuse-lite@1.0.30001751: {} caniuse-lite@1.0.30001752: {} @@ -17829,6 +18036,14 @@ snapshots: crypto-random-string@2.0.0: {} + css-background-parser@0.1.0: {} + + css-box-shadow@1.0.0-3: {} + + css-color-keywords@1.0.0: {} + + css-gradient-parser@0.0.17: {} + css-in-js-utils@3.1.0: dependencies: hyphenate-style-name: 1.1.0 @@ -17849,6 +18064,12 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + css-tree@1.1.3: dependencies: mdn-data: 2.0.14 @@ -18232,6 +18453,8 @@ snapshots: emoji-mart@5.6.0: {} + emoji-regex-xs@2.0.1: {} + emoji-regex@10.4.0: {} emoji-regex@10.5.0: {} @@ -18451,6 +18674,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -19048,6 +19273,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.7.4: {} + fflate@0.8.2: {} figures@6.1.0: @@ -19511,6 +19738,8 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hex-rgb@4.3.0: {} + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -20036,6 +20265,11 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + lines-and-columns@1.2.4: {} linkedom@0.18.12: @@ -21054,6 +21288,8 @@ snapshots: package-manager-detector@1.5.0: {} + pako@0.2.9: {} + pako@2.1.0: {} param-case@3.0.4: @@ -21069,6 +21305,11 @@ snapshots: dependencies: author-regex: 1.0.0 + parse-css-color@0.2.1: + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -22498,6 +22739,20 @@ snapshots: safer-buffer@2.1.2: {} + satori@0.18.3: + dependencies: + '@shuding/opentype.js': 1.4.0-beta.0 + css-background-parser: 0.1.0 + css-box-shadow: 1.0.0-3 + css-gradient-parser: 0.0.17 + css-to-react-native: 3.2.0 + emoji-regex-xs: 2.0.1 + escape-html: 1.0.3 + linebreak: 1.1.0 + parse-css-color: 0.2.1 + postcss-value-parser: 4.2.0 + yoga-layout: 3.2.1 + scheduler@0.27.0: {} screenfull@5.2.0: {} @@ -23252,6 +23507,11 @@ snapshots: unicode-property-aliases-ecmascript@2.2.0: {} + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + unicorn-magic@0.1.0: {} unicorn-magic@0.3.0: {} @@ -23896,6 +24156,8 @@ snapshots: yoctocolors@2.1.1: {} + yoga-layout@3.2.1: {} + zod-validation-error@3.5.3(zod@3.25.76): dependencies: zod: 3.25.76