feat: support share to external via iframe

- Added Tailwind CSS plugins and configurations to improve styling across the application.
- Introduced new global styles and layout components for better structure and design consistency.
- Implemented a MasonryGallery component for responsive photo display.
- Enhanced photo item rendering with EXIF data and improved loading states.
- Updated package dependencies to include new Tailwind CSS utilities and components.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-06-29 23:09:57 +08:00
parent 5d523af3bc
commit 96400b1775
35 changed files with 1097 additions and 125 deletions

42
apps/ssr/global.d.ts vendored Normal file
View File

@@ -0,0 +1,42 @@
/* eslint-disable @typescript-eslint/no-empty-object-type */
import type { FC, PropsWithChildren } from 'react'
declare global {
export type NextErrorProps = {
reset: () => void
error: Error
}
export type NextPageParams<P extends {}, Props = {}> = PropsWithChildren<
{
params: Promise<P>
searchParams: Promise<Record<string, string | string[] | undefined>>
} & Props
>
export type NextPageExtractedParams<
P extends {},
Props = {},
> = PropsWithChildren<
{
params: P
searchParams: Promise<Record<string, string | string[] | undefined>>
} & Props
>
export type Component<P = {}> = FC<ComponentType & P>
export type ComponentType<P = {}> = {
className?: string
} & PropsWithChildren &
P
}
declare module 'react' {
export interface AriaAttributes {
'data-hide-print'?: boolean
'data-event'?: string
'data-testid'?: string
}
}
export {}

View File

@@ -19,17 +19,32 @@
"start": "next start"
},
"dependencies": {
"@afilmory/components": "workspace:*",
"@afilmory/data": "workspace:*",
"@t3-oss/env-nextjs": "0.13.8",
"clsx": "2.1.1",
"drizzle-orm": "0.44.2",
"es-toolkit": "1.39.5",
"linkedom": "0.18.11",
"pg": "8.16.2",
"postgres": "3.4.7",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-masonry": "1.0.7",
"react-photo-view": "1.2.7",
"react-responsive-masonry": "2.7.1",
"react-use": "17.6.0",
"tailwind-merge": "3.3.1",
"tailwind-variants": "catalog:",
"thumbhash": "0.1.1",
"usehooks-ts": "3.1.1",
"zod": "catalog:"
},
"devDependencies": {
"@egoist/tailwindcss-icons": "catalog:",
"@iconify-json/mingcute": "catalog:",
"@tailwindcss/postcss": "catalog:",
"@tailwindcss/typography": "catalog:",
"@types/node": "24.0.4",
"@types/pg": "8.15.4",
"@types/react": "19.1.8",
@@ -38,6 +53,12 @@
"cross-env": "7.0.3",
"dotenv-expand": "catalog:",
"drizzle-kit": "0.31.2",
"next": "15.3.4"
"next": "15.3.4",
"postcss": "8.5.6",
"tailwind-scrollbar": "catalog:",
"tailwindcss": "catalog:",
"tailwindcss-animate": "catalog:",
"tailwindcss-safe-area": "catalog:",
"tailwindcss-uikit-colors": "catalog:"
}
}

View File

@@ -0,0 +1,6 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default config

View File

@@ -0,0 +1,7 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
@plugin 'tailwind-scrollbar';
@plugin 'tailwindcss-animate';
@plugin 'tailwindcss-safe-area';
@import 'tailwindcss-uikit-colors/v4/macos.css';
@plugin '@egoist/tailwindcss-icons';

View File

@@ -0,0 +1,13 @@
import './globals.css'
import { RootProviders } from '~/providers'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<RootProviders>{children}</RootProviders>
</body>
</html>
)
}

View File

@@ -0,0 +1,31 @@
'use client'
import type { PhotoManifestItem } from '@afilmory/builder'
import { useMemo } from 'react'
import Masonry from 'react-responsive-masonry'
import { useWindowSize } from 'usehooks-ts'
import { PhotoItem } from './PhotoItem'
interface MasonryGalleryProps {
photos: PhotoManifestItem[]
}
export function MasonryGallery({ photos }: MasonryGalleryProps) {
const { width } = useWindowSize()
const columnsCount = useMemo(() => {
if (width < 600) return 1
if (width < 800) return 2
return 3
}, [width])
return (
<div className="scrollbar-none h-screen overflow-auto">
<Masonry gutter={4} columnsCount={columnsCount}>
{photos.map((photo) => (
<PhotoItem key={photo.id} photo={photo} />
))}
</Masonry>
</div>
)
}

View File

@@ -0,0 +1,201 @@
'use client'
import type { PhotoManifestItem } from '@afilmory/builder'
import {
CarbonIsoOutline,
MaterialSymbolsShutterSpeed,
StreamlineImageAccessoriesLensesPhotosCameraShutterPicturePhotographyPicturesPhotoLens,
TablerAperture,
} from '@afilmory/components/icons/index.tsx'
import { thumbHashToDataURL } from 'thumbhash'
import { cn } from '~/lib/cn'
import { url } from '../../../../../../../config.json'
const decompressUint8Array = (compressed: string) => {
return Uint8Array.from(
compressed.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16)),
)
}
interface PhotoItemProps {
photo: PhotoManifestItem
className?: string
}
export function PhotoItem({ photo, className }: PhotoItemProps) {
// 生成 thumbhash 预览
const thumbHashDataURL = photo.thumbHash
? thumbHashToDataURL(decompressUint8Array(photo.thumbHash))
: null
const ratio = photo.aspectRatio
// 格式化 EXIF 数据
const formatExifData = () => {
const { exif } = photo
// 安全处理:如果 exif 不存在或为空,则返回空对象
if (!exif) {
return {
focalLength35mm: null,
iso: null,
shutterSpeed: null,
aperture: null,
}
}
// 等效焦距 (35mm)
const focalLength35mm = exif.FocalLengthIn35mmFormat
? Number.parseInt(exif.FocalLengthIn35mmFormat)
: exif.FocalLength
? Number.parseInt(exif.FocalLength)
: null
// ISO
const iso = exif.ISO
// 快门速度
const exposureTime = exif.ExposureTime
const shutterSpeed = exposureTime ? `${exposureTime}s` : null
// 光圈
const aperture = exif.FNumber ? `f/${exif.FNumber}` : null
return {
focalLength35mm,
iso,
shutterSpeed,
aperture,
}
}
const exifData = formatExifData()
return (
<button
type="button"
role="link"
onClick={() => {
window.open(`${url}/${photo.id}`, '_blank')
}}
className={cn(
'group relative block w-full cursor-pointer overflow-hidden text-left',
className,
)}
style={{
paddingTop: `${100 / ratio}%`,
}}
>
<div
className={cn(
'pointer-events-none absolute inset-0 z-[1] flex items-start justify-center',
)}
>
<div className="bg-material-medium mt-4 flex items-center gap-2 rounded-full border border-white/20 px-3 py-1.5 opacity-0 backdrop-blur-[70px] transition-opacity duration-300 group-hover:opacity-100">
<i className="i-mingcute-external-link-line size-4 text-white" />
<span className="text-sm text-white/80">Open in AFilmory</span>
</div>
</div>
<div className="absolute inset-0">
<img
src={thumbHashDataURL}
alt={photo.title}
className="absolute inset-0 size-full"
loading="lazy"
/>
<img
src={photo.originalUrl}
alt={photo.title}
className="absolute inset-0 size-full object-cover object-center"
loading="lazy"
/>
</div>
{/* 图片信息和 EXIF 覆盖层 */}
<div className="@container pointer-events-none">
{/* 渐变背景 - 独立的层 */}
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
{/* 内容层 - 独立的层以支持 backdrop-filter */}
<div className="absolute inset-x-0 bottom-0 p-4 pb-0 text-white">
{/* 基本信息和标签 section */}
<div className="mb-3 [&_*]:duration-300">
<div className="items-center justify-between @[600px]:flex">
<div>
<h3 className="mb-2 truncate text-sm font-medium opacity-0 group-hover:opacity-100">
{photo.title}
</h3>
{photo.description && (
<p className="mb-2 line-clamp-2 text-sm text-white/80 opacity-0 group-hover:opacity-100">
{photo.description}
</p>
)}
</div>
{/* 基本信息 */}
<div>
<div className="mb-2 flex flex-wrap gap-2 text-xs text-white/80 opacity-0 group-hover:opacity-100">
<span>
{photo.width} × {photo.height}
</span>
<span></span>
<span>{(photo.size / 1024 / 1024).toFixed(1)}MB</span>
</div>
</div>
</div>
{/* Tags */}
{photo.tags && photo.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{photo.tags.map((tag) => (
<span
key={tag}
className="rounded-full bg-white/20 px-2 py-0.5 text-xs text-white/90 opacity-0 backdrop-blur-sm group-hover:opacity-100"
>
{tag}
</span>
))}
</div>
)}
</div>
<div className="grid grid-cols-2 gap-2 pb-4 text-xs @[600px]:grid-cols-4">
{exifData.focalLength35mm && (
<div className="flex items-center gap-1.5 rounded-md bg-white/10 px-2 py-1 opacity-0 backdrop-blur-md transition-opacity duration-300 group-hover:opacity-100">
<StreamlineImageAccessoriesLensesPhotosCameraShutterPicturePhotographyPicturesPhotoLens className="text-white/70" />
<span className="text-white/90">
{exifData.focalLength35mm}mm
</span>
</div>
)}
{exifData.aperture && (
<div className="flex items-center gap-1.5 rounded-md bg-white/10 px-2 py-1 opacity-0 backdrop-blur-md transition-opacity duration-300 group-hover:opacity-100">
<TablerAperture className="text-white/70" />
<span className="text-white/90">{exifData.aperture}</span>
</div>
)}
{exifData.shutterSpeed && (
<div className="flex items-center gap-1.5 rounded-md bg-white/10 px-2 py-1 opacity-0 backdrop-blur-md transition-opacity duration-300 group-hover:opacity-100">
<MaterialSymbolsShutterSpeed className="text-white/70" />
<span className="text-white/90">{exifData.shutterSpeed}</span>
</div>
)}
{exifData.iso && (
<div className="flex items-center gap-1.5 rounded-md bg-white/10 px-2 py-1 opacity-0 backdrop-blur-md transition-opacity duration-300 group-hover:opacity-100">
<CarbonIsoOutline className="text-white/70" />
<span className="text-white/90">ISO {exifData.iso}</span>
</div>
)}
</div>
</div>
</div>
</button>
)
}

View File

@@ -0,0 +1,23 @@
'use client'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<style jsx global>{`
body {
color: #ffffff;
font-family: -apple-system, system-ui, sans-serif;
overflow-x: hidden;
}
html,
body {
position: fixed;
inset: 0;
margin: 0;
padding: 0;
background: #0a0a0a;
}
`}</style>
{children}
</>
)
}

View File

@@ -0,0 +1,44 @@
import type { PhotoManifestItem } from '@afilmory/builder'
import { notFound } from 'next/navigation'
import { photoLoader } from '~/lib/photo-loader'
import { MasonryGallery } from './MasonryGallery'
import { PhotoItem } from './PhotoItem'
export default async function Page({
searchParams,
}: NextPageExtractedParams<unknown>) {
const { id } = await searchParams
let photos: PhotoManifestItem[] = []
if (!id) return notFound()
if (typeof id === 'string') {
const photo = await photoLoader.getPhoto(id)
if (!photo) {
notFound()
}
photos = [photo]
} else {
photos = await photoLoader.getPhotos(id)
if (photos.length === 0) {
notFound()
}
}
if (photos.length === 1) {
return (
<PhotoItem
photo={photos[0]}
className="absolute inset-0 size-full !pt-0"
/>
)
}
return (
<div className="h-screen bg-[#0a0a0a] text-white">
<MasonryGallery photos={photos} />
</div>
)
}

5
apps/ssr/src/lib/cn.ts Normal file
View File

@@ -0,0 +1,5 @@
import type { ClassValue } from 'clsx'
import clsx from 'clsx'
import { twMerge } from 'tailwind-merge'
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs))

View File

@@ -12,6 +12,7 @@ export const injectConfigToDocument = (document: OnlyHTMLDocument) => {
const $config = document.head.querySelector('#config')
const injectConfigBase = {
useApi: DbManager.shared.isEnabled(),
useNext: true,
}
if ($config) {
$config.innerHTML = `window.__CONFIG__ = ${JSON.stringify(injectConfigBase)}`

View File

@@ -17,7 +17,10 @@ class PhotoLoader {
})
}
getPhotos() {
getPhotos(ids?: string[]) {
if (ids) {
return this.photos.filter((photo) => ids.includes(photo.id))
}
return this.photos
}

View File

@@ -0,0 +1,3 @@
export const RootProviders = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>
}

View File

@@ -16,6 +16,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"allowImportingTsExtensions": true,
"jsx": "preserve",
"plugins": [
{

View File

@@ -69,6 +69,7 @@
"swiper": "11.2.8",
"swr": "2.3.3",
"tailwind-merge": "3.3.1",
"tailwind-variants": "1.0.0",
"thumbhash": "0.1.1",
"tiff": "^7.0.0",
"usehooks-ts": "3.1.1",
@@ -77,11 +78,10 @@
"zustand": "5.0.5"
},
"devDependencies": {
"@egoist/tailwindcss-icons": "1.9.0",
"@iconify-json/mingcute": "1.2.3",
"@tailwindcss/container-queries": "0.1.1",
"@tailwindcss/postcss": "4.1.11",
"@tailwindcss/typography": "0.5.16",
"@egoist/tailwindcss-icons": "catalog:",
"@iconify-json/mingcute": "catalog:",
"@tailwindcss/postcss": "catalog:",
"@tailwindcss/typography": "catalog:",
"@tailwindcss/vite": "4.1.11",
"@types/node": "24.0.4",
"@types/react": "19.1.8",
@@ -92,17 +92,17 @@
"code-inspector-plugin": "0.20.12",
"daisyui": "5.0.43",
"execa": "9.6.0",
"kolorist": "1.8.0",
"postcss": "8.5.6",
"postcss-import": "16.1.1",
"postcss-js": "4.0.1",
"react-compiler-runtime": "19.1.0-rc.2",
"simple-git-hooks": "2.13.0",
"tailwind-scrollbar": "4.0.2",
"tailwind-variants": "1.0.0",
"tailwindcss": "4.1.11",
"tailwindcss-animate": "1.0.7",
"tailwindcss-safe-area": "0.6.0",
"tailwindcss-uikit-colors": "1.0.0-alpha.1",
"tailwind-scrollbar": "catalog:",
"tailwindcss": "catalog:",
"tailwindcss-animate": "catalog:",
"tailwindcss-safe-area": "catalog:",
"tailwindcss-uikit-colors": "catalog:",
"unplugin-ast": "0.15.0",
"vite-plugin-html": "3.2.2"
}

View File

@@ -156,59 +156,64 @@ export const ExifPanel: FC<{
)}
</div>
{formattedExifData && (
<div>
<h4 className="my-2 text-sm font-medium text-white/80">
{t('exif.capture.parameters')}
</h4>
<div className={`grid grid-cols-2 gap-2`}>
{formattedExifData.focalLength35mm && (
<div className="flex h-6 items-center gap-2 rounded-md bg-white/10 px-2">
<StreamlineImageAccessoriesLensesPhotosCameraShutterPicturePhotographyPicturesPhotoLens className="text-sm text-white/70" />
<span className="text-xs">
{formattedExifData.focalLength35mm}mm
</span>
</div>
)}
{formattedExifData &&
(formattedExifData.shutterSpeed ||
formattedExifData.iso ||
formattedExifData.aperture ||
formattedExifData.exposureBias ||
formattedExifData.focalLength35mm) && (
<div>
<h4 className="my-2 text-sm font-medium text-white/80">
{t('exif.capture.parameters')}
</h4>
<div className={`grid grid-cols-2 gap-2`}>
{formattedExifData.focalLength35mm && (
<div className="flex h-6 items-center gap-2 rounded-md bg-white/10 px-2">
<StreamlineImageAccessoriesLensesPhotosCameraShutterPicturePhotographyPicturesPhotoLens className="text-sm text-white/70" />
<span className="text-xs">
{formattedExifData.focalLength35mm}mm
</span>
</div>
)}
{formattedExifData.aperture && (
<div className="flex h-6 items-center gap-2 rounded-md bg-white/10 px-2">
<TablerAperture className="text-sm text-white/70" />
<span className="text-xs">
{formattedExifData.aperture}
</span>
</div>
)}
{formattedExifData.aperture && (
<div className="flex h-6 items-center gap-2 rounded-md bg-white/10 px-2">
<TablerAperture className="text-sm text-white/70" />
<span className="text-xs">
{formattedExifData.aperture}
</span>
</div>
)}
{formattedExifData.shutterSpeed && (
<div className="flex h-6 items-center gap-2 rounded-md bg-white/10 px-2">
<MaterialSymbolsShutterSpeed className="text-sm text-white/70" />
<span className="text-xs">
{formattedExifData.shutterSpeed}
</span>
</div>
)}
{formattedExifData.shutterSpeed && (
<div className="flex h-6 items-center gap-2 rounded-md bg-white/10 px-2">
<MaterialSymbolsShutterSpeed className="text-sm text-white/70" />
<span className="text-xs">
{formattedExifData.shutterSpeed}
</span>
</div>
)}
{formattedExifData.iso && (
<div className="flex h-6 items-center gap-2 rounded-md bg-white/10 px-2">
<CarbonIsoOutline className="text-sm text-white/70" />
<span className="text-xs">
ISO {formattedExifData.iso}
</span>
</div>
)}
{formattedExifData.iso && (
<div className="flex h-6 items-center gap-2 rounded-md bg-white/10 px-2">
<CarbonIsoOutline className="text-sm text-white/70" />
<span className="text-xs">
ISO {formattedExifData.iso}
</span>
</div>
)}
{formattedExifData.exposureBias && (
<div className="flex h-6 items-center gap-2 rounded-md bg-white/10 px-2">
<MaterialSymbolsExposure className="text-sm text-white/70" />
<span className="text-xs">
{formattedExifData.exposureBias}
</span>
</div>
)}
{formattedExifData.exposureBias && (
<div className="flex h-6 items-center gap-2 rounded-md bg-white/10 px-2">
<MaterialSymbolsExposure className="text-sm text-white/70" />
<span className="text-xs">
{formattedExifData.exposureBias}
</span>
</div>
)}
</div>
</div>
</div>
)}
)}
{/* 标签信息 - 移到基本信息 section 内 */}
{currentPhoto.tags && currentPhoto.tags.length > 0 && (

View File

@@ -1,9 +1,11 @@
import { siteConfig } from '@config'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { AnimatePresence, m } from 'motion/react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { injectConfig } from '~/config'
import { clsxm } from '~/lib/cn'
import { Spring } from '~/lib/spring'
import type { PhotoManifest } from '~/types/photo'
@@ -121,6 +123,23 @@ export const SharePanel = ({ photo, trigger, blobSrc }: SharePanelProps) => {
}
}, [t])
const handleCopyEmbedCode = useCallback(async () => {
try {
const embedCode = `<iframe
src="${siteConfig.url}/share/iframe?id=${photo.id}"
height="500"
className="w-full"
allowTransparency
sandbox="allow-scripts allow-same-origin allow-popups"
/>`
await navigator.clipboard.writeText(embedCode)
toast.success(t('photo.share.embed.copied'))
setIsOpen(false)
} catch {
toast.error(t('photo.share.copy.failed'))
}
}, [photo.id, t])
const handleSocialShare = useCallback(
(url: string) => {
const shareUrl = encodeURIComponent(window.location.href)
@@ -160,6 +179,13 @@ export const SharePanel = ({ photo, trigger, blobSrc }: SharePanelProps) => {
icon: 'i-mingcute-link-line',
action: handleCopyLink,
},
{
id: 'copy-embed',
label: t('photo.share.embed.code'),
icon: 'i-mingcute-code-line',
action: handleCopyEmbedCode,
color: 'text-purple-500',
},
]
return (
@@ -208,7 +234,7 @@ export const SharePanel = ({ photo, trigger, blobSrc }: SharePanelProps) => {
{t('photo.share.social.media')}
</h4>
</div>
<div className="flex justify-center gap-4">
<div className="flex gap-6 px-2">
{socialOptions.map((option) => (
<button
key={option.id}
@@ -240,7 +266,45 @@ export const SharePanel = ({ photo, trigger, blobSrc }: SharePanelProps) => {
</div>
</div>
{/* 功能选项 - 第二排 */}
{/* 嵌入代码 - 第二排 */}
{injectConfig.useNext && (
<div className="mb-6">
<div className="mb-3">
<h4 className="text-text-secondary text-xs font-medium tracking-wide uppercase">
{t('photo.share.embed.code')}
</h4>
<p className="text-text-tertiary mt-1 text-xs">
{t('photo.share.embed.description')}
</p>
</div>
<div className="relative">
<div className="bg-fill-secondary/50 border-border/10 rounded-lg border p-3">
<code className="text-text-secondary font-mono text-xs break-all whitespace-pre select-all">
{`<iframe
src="${siteConfig.url}/share/iframe?id=${photo.id}"
height="500"
style="width: 100%;"
allowTransparency
sandbox="allow-scripts allow-same-origin allow-popups"
/>`}
</code>
</div>
<button
type="button"
className={clsxm(
'absolute top-2 right-2 flex items-center justify-center',
'size-7 rounded-md bg-fill-tertiary/80 hover:bg-fill-tertiary backdrop-blur-3xl',
'transition-colors duration-200 group',
)}
onClick={handleCopyEmbedCode}
>
<i className="i-mingcute-copy-line text-text-secondary group-hover:text-text size-3.5" />
</button>
</div>
</div>
)}
{/* 功能选项 - 第三排 */}
<div>
<div className="mb-3">
<h4 className="text-text-secondary text-xs font-medium tracking-wide uppercase">
@@ -248,40 +312,42 @@ export const SharePanel = ({ photo, trigger, blobSrc }: SharePanelProps) => {
</h4>
</div>
<div className="grid grid-cols-2 gap-1">
{actionOptions.map((option) => (
<button
key={option.id}
type="button"
className={clsxm(
'relative flex cursor-pointer select-none items-center rounded-lg px-2 py-2',
'text-sm outline-none transition-all duration-200',
'hover:bg-fill-secondary/80 active:bg-fill-secondary',
'group',
)}
onClick={() => option.action()}
>
<div className="flex items-center gap-2">
<div
className={clsxm(
'flex size-7 items-center justify-center rounded-full',
'bg-fill-tertiary/80 group-hover:bg-fill-tertiary',
'transition-colors duration-200',
)}
>
<i
{actionOptions
.filter((option) => option.id !== 'copy-embed')
.map((option) => (
<button
key={option.id}
type="button"
className={clsxm(
'relative flex cursor-pointer select-none items-center rounded-lg px-2 py-2',
'text-sm outline-none transition-all duration-200',
'hover:bg-fill-secondary/80 active:bg-fill-secondary',
'group',
)}
onClick={() => option.action()}
>
<div className="flex items-center gap-2">
<div
className={clsxm(
option.icon,
'size-3.5',
option.color || 'text-text-secondary',
'flex size-7 items-center justify-center rounded-full',
'bg-fill-tertiary/80 group-hover:bg-fill-tertiary',
'transition-colors duration-200',
)}
/>
>
<i
className={clsxm(
option.icon,
'size-3.5',
option.color || 'text-text-secondary',
)}
/>
</div>
<span className="text-text text-xs font-medium">
{option.label}
</span>
</div>
<span className="text-text text-xs font-medium">
{option.label}
</span>
</div>
</button>
))}
</button>
))}
</div>
</div>
</m.div>

View File

@@ -215,7 +215,11 @@ export const formatExifData = (exif: PickedExif | null) => {
// 快门速度
const exposureTime = exif.ExposureTime
const shutterSpeed = `${exposureTime}s`
const shutterSpeed = exposureTime
? `${exposureTime}s`
: exif.ShutterSpeedValue
? `${exif.ShutterSpeedValue}s`
: null
// 光圈
const aperture = exif.FNumber ? `f/${exif.FNumber}` : null

View File

@@ -2,6 +2,7 @@ import { merge } from 'es-toolkit/compat'
const defaultInjectConfig = {
useApi: false,
useNext: false,
}
export const injectConfig = merge(defaultInjectConfig, __CONFIG__)

View File

@@ -1 +0,0 @@
export { domMax as default } from 'motion/react'

View File

@@ -31,7 +31,7 @@ export function CarbonIsoOutline(props: SVGProps<SVGSVGElement>) {
viewBox="0 0 32 32"
{...props}
>
{/* Icon from Carbon by IBM - undefined */}
{/* Icon from Carbon by IBM */}
<path
fill="currentColor"
d="M24 21h-3a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2m-3-8v6h3v-6zm-6 8h-5v-2h5v-2h-3a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h5v2h-5v2h3a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2M6 11h2v10H6z"

View File

@@ -15,7 +15,7 @@ import { ImageLoaderManager } from '~/lib/image-loader-manager'
import { getImageFormat } from '~/lib/image-utils'
import type { PhotoManifest } from '~/types/photo'
export const PhotoMasonryItem = ({
export const MasonryPhotoItem = ({
data,
width,
index: _,
@@ -88,7 +88,7 @@ export const PhotoMasonryItem = ({
// 快门速度
const exposureTime = exif.ExposureTime
const shutterSpeed = `${exposureTime}s`
const shutterSpeed = exposureTime ? `${exposureTime}s` : null
// 光圈
const aperture = exif.FNumber ? `f/${exif.FNumber}` : null

View File

@@ -18,7 +18,7 @@ import { ActionGroup, ActionPanel } from './ActionGroup'
import { FloatingActionButton } from './FloatingActionButton'
import { Masonry } from './Masonic'
import { MasonryHeaderMasonryItem } from './MasonryHeaderMasonryItem'
import { PhotoMasonryItem } from './PhotoMasonryItem'
import { MasonryPhotoItem } from './MasonryPhotoItem'
class MasonryHeaderItem {
static default = new MasonryHeaderItem()
@@ -273,7 +273,7 @@ export const MasonryItem = memo(
animate="visible"
onAnimationComplete={shouldAnimate ? onAnimationComplete : undefined}
>
<PhotoMasonryItem
<MasonryPhotoItem
data={data as PhotoManifest}
width={width}
index={index}

View File

@@ -0,0 +1,33 @@
import { ScrollArea } from '~/components/ui/scroll-areas/ScrollArea'
export const Component = () => {
return (
<ScrollArea rootClassName="h-screen">
<div className="mx-auto w-[60ch] py-8">
<iframe
src="http://localhost:1924/share/iframe?id=DSCF0842"
height={500}
className="w-full"
allowTransparency
sandbox="allow-scripts allow-same-origin allow-popups"
/>
<iframe
src="http://localhost:1924/share/iframe?id=DSCF0842"
height={400}
className="w-[400px]"
allowTransparency
sandbox="allow-scripts allow-same-origin allow-popups"
/>
<iframe
src="http://localhost:1924/share/iframe?id=IMG_0030&id=DSCF0842"
height={400}
className="w-full"
allowTransparency
sandbox="allow-scripts allow-same-origin allow-popups"
/>
</div>
</ScrollArea>
)
}

View File

@@ -6,6 +6,8 @@ import { fileURLToPath } from 'node:url'
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import { codeInspectorPlugin } from 'code-inspector-plugin'
import { cyan, dim, green } from 'kolorist'
import type { PluginOption, ViteDevServer } from 'vite'
import { defineConfig } from 'vite'
import { analyzer } from 'vite-bundle-analyzer'
import { checker } from 'vite-plugin-checker'
@@ -21,6 +23,19 @@ import { localesJsonPlugin } from './plugins/vite/locales-json'
import { manifestInjectPlugin } from './plugins/vite/manifest-inject'
import { ogImagePlugin } from './plugins/vite/og-image-plugin'
const devPrint = (): PluginOption => ({
name: 'dev-print',
configureServer(server: ViteDevServer) {
server.printUrls = () => {
console.info(
` ${green('➜')} ${dim('Next.js SSR')}: ${cyan(
'http://localhost:1924',
)}`,
)
}
},
})
const __dirname = path.dirname(fileURLToPath(import.meta.url))
if (process.env.CI) {
@@ -90,6 +105,8 @@ export default defineConfig({
},
}),
process.env.analyzer && analyzer(),
devPrint(),
],
server: {
port: !DEV_NEXT_JS ? 1924 : 3000, // 1924 年首款 35mm 相机问世

View File

@@ -235,6 +235,9 @@
"photo.share.copy.failed": "Copy failed",
"photo.share.copy.link": "Copy Link",
"photo.share.default.title": "Photo Share",
"photo.share.embed.code": "Embed Code",
"photo.share.embed.copied": "Embed code copied to clipboard",
"photo.share.embed.description": "Copy this code to embed the photo on your website",
"photo.share.link.copied": "Link copied to clipboard",
"photo.share.social.media": "Social Media",
"photo.share.system": "System Share",

View File

@@ -233,6 +233,9 @@
"photo.share.copy.failed": "コピーに失敗しました",
"photo.share.copy.link": "リンクをコピー",
"photo.share.default.title": "写真の共有",
"photo.share.embed.code": "埋め込みコード",
"photo.share.embed.copied": "埋め込みコードがクリップボードにコピーされました",
"photo.share.embed.description": "このコードをコピーして、あなたのウェブサイトに写真を埋め込んでください",
"photo.share.link.copied": "リンクがクリップボードにコピーされました",
"photo.share.social.media": "ソーシャルメディア",
"photo.share.system": "システム共有",

View File

@@ -233,6 +233,9 @@
"photo.share.copy.failed": "복사 실패",
"photo.share.copy.link": "링크 복사",
"photo.share.default.title": "사진 공유",
"photo.share.embed.code": "임베드 코드",
"photo.share.embed.copied": "임베드 코드를 클립보드에 복사했습니다",
"photo.share.embed.description": "이 코드를 복사하여 웹사이트에 사진을 임베드하세요",
"photo.share.link.copied": "링크를 클립보드에 복사했습니다",
"photo.share.social.media": "소셜 미디어",
"photo.share.system": "시스템 공유",

View File

@@ -235,6 +235,9 @@
"photo.share.copy.failed": "复制失败",
"photo.share.copy.link": "复制链接",
"photo.share.default.title": "照片分享",
"photo.share.embed.code": "嵌入代码",
"photo.share.embed.copied": "嵌入代码已复制到剪贴板",
"photo.share.embed.description": "复制此代码以在您的网站中嵌入照片",
"photo.share.link.copied": "链接已复制到剪贴板",
"photo.share.social.media": "社交媒体",
"photo.share.system": "系统分享",

View File

@@ -233,6 +233,9 @@
"photo.share.copy.failed": "複製失敗",
"photo.share.copy.link": "複製連結",
"photo.share.default.title": "照片分享",
"photo.share.embed.code": "嵌入代碼",
"photo.share.embed.copied": "嵌入代碼已複製到剪貼簿",
"photo.share.embed.description": "複製此代碼以在您的網站中嵌入照片",
"photo.share.link.copied": "連結已複製到剪貼簿",
"photo.share.social.media": "社交媒體",
"photo.share.system": "系統分享",

View File

@@ -233,6 +233,9 @@
"photo.share.copy.failed": "複製失敗",
"photo.share.copy.link": "複製連結",
"photo.share.default.title": "照片分享",
"photo.share.embed.code": "嵌入代碼",
"photo.share.embed.copied": "嵌入代碼已複製到剪貼簿",
"photo.share.embed.description": "複製此代碼以在您的網站中嵌入照片",
"photo.share.link.copied": "連結已複製到剪貼簿",
"photo.share.social.media": "社群媒體",
"photo.share.system": "系統分享",

View File

@@ -0,0 +1,11 @@
{
"name": "@afilmory/components",
"type": "module",
"private": true,
"exports": {
"./*": "./src/*"
},
"peerDependencies": {
"react": "^19"
}
}

View File

@@ -0,0 +1,107 @@
import type { SVGProps } from 'react'
export function TablerAperture(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
{/* Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE */}
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0m.6 3h10.55M6.551 4.938l3.26 10.034m7.221-10.336l-8.535 6.201m12.062 3.673l-8.535-6.201m.233 12.607l3.261-10.034"
/>
</svg>
)
}
export function CarbonIsoOutline(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 32 32"
{...props}
>
{/* Icon from Carbon by IBM */}
<path
fill="currentColor"
d="M24 21h-3a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2m-3-8v6h3v-6zm-6 8h-5v-2h5v-2h-3a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h5v2h-5v2h3a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2M6 11h2v10H6z"
/>
<path
fill="currentColor"
d="M28 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2M4 24V8h24v16Z"
/>
</svg>
)
}
export function MaterialSymbolsShutterSpeed(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
{/* Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE */}
<path
fill="currentColor"
d="M9 3V1h6v2zm3 19q-1.875 0-3.512-.712T5.625 19.35T3.7 16.487T3 13t.713-3.488T5.65 6.65t2.863-1.937T12 4q1.575 0 3 .525T17.6 6l1.45-1.45l1.4 1.4l-1.4 1.45q.9 1.175 1.425 2.6T21 13q0 1.85-.7 3.488t-1.925 2.862t-2.863 1.938T12 22m0-2q2.925 0 4.963-2.037T19 13t-2.037-4.962T12 6T7.038 8.038T5 13t2.038 4.963T12 20m0-9h5.65q-.45-1.275-1.4-2.238T14.1 7.375zm-1.725 1L13.1 7.1q-1.275-.25-2.562.075t-2.363 1.2zM6.1 14h4.175L7.45 9.1q-.875.975-1.225 2.263T6.1 14m3.8 4.625L12 15H6.35q.425 1.25 1.388 2.225t2.162 1.4m1 .275q1.425.275 2.725-.112t2.2-1.163L13.725 14zm5.65-2q.9-1.025 1.238-2.287T17.9 12h-4.175z"
/>
</svg>
)
}
export function StreamlineImageAccessoriesLensesPhotosCameraShutterPicturePhotographyPicturesPhotoLens(
props: SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 14 14"
{...props}
>
{/* Icon from Streamline by Streamline - https://creativecommons.org/licenses/by/4.0/ */}
<g
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="7" cy="7" r="6.5" />
<circle cx="7" cy="7" r="2.5" />
<path d="M4.5 7V1M7 4.5h6M9.5 7v6M7 9.5H1" />
</g>
</svg>
)
}
export function MaterialSymbolsExposure(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
{/* Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE */}
<path
fill="currentColor"
d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5zm9.5-1v-2h-2v-1.5h2v-2H16v2h2V16h-2v2zM6 8.5h5V7H6z"
/>
</svg>
)
}

348
pnpm-lock.yaml generated
View File

@@ -6,12 +6,42 @@ settings:
catalogs:
default:
'@egoist/tailwindcss-icons':
specifier: 1.9.0
version: 1.9.0
'@iconify-json/mingcute':
specifier: 1.2.3
version: 1.2.3
'@t3-oss/env-core':
specifier: 0.13.8
version: 0.13.8
'@tailwindcss/postcss':
specifier: 4.1.11
version: 4.1.11
'@tailwindcss/typography':
specifier: 0.5.16
version: 0.5.16
dotenv-expand:
specifier: 12.0.2
version: 12.0.2
tailwind-scrollbar:
specifier: 4.0.2
version: 4.0.2
tailwind-variants:
specifier: 1.0.0
version: 1.0.0
tailwindcss:
specifier: 4.1.11
version: 4.1.11
tailwindcss-animate:
specifier: 1.0.7
version: 1.0.7
tailwindcss-safe-area:
specifier: 0.6.0
version: 0.6.0
tailwindcss-uikit-colors:
specifier: 1.0.0-alpha.1
version: 1.0.0-alpha.1
typescript:
specifier: 5.8.3
version: 5.8.3
@@ -99,15 +129,24 @@ importers:
apps/ssr:
dependencies:
'@afilmory/components':
specifier: workspace:*
version: link:../../packages/components
'@afilmory/data':
specifier: workspace:*
version: link:../../packages/data
'@t3-oss/env-nextjs':
specifier: 0.13.8
version: 0.13.8(typescript@5.8.3)(zod@3.25.67)
clsx:
specifier: 2.1.1
version: 2.1.1
drizzle-orm:
specifier: 0.44.2
version: 0.44.2(@types/pg@8.15.4)(@vercel/postgres@0.10.0)(pg@8.16.2)(postgres@3.4.7)
es-toolkit:
specifier: 1.39.5
version: 1.39.5
linkedom:
specifier: 0.18.11
version: 0.18.11
@@ -123,10 +162,46 @@ importers:
react-dom:
specifier: 19.1.0
version: 19.1.0(react@19.1.0)
react-masonry:
specifier: 1.0.7
version: 1.0.7
react-photo-view:
specifier: 1.2.7
version: 1.2.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-responsive-masonry:
specifier: 2.7.1
version: 2.7.1
react-use:
specifier: 17.6.0
version: 17.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
tailwind-merge:
specifier: 3.3.1
version: 3.3.1
tailwind-variants:
specifier: 'catalog:'
version: 1.0.0(tailwindcss@4.1.11)
thumbhash:
specifier: 0.1.1
version: 0.1.1
usehooks-ts:
specifier: 3.1.1
version: 3.1.1(react@19.1.0)
zod:
specifier: 'catalog:'
version: 3.25.67
devDependencies:
'@egoist/tailwindcss-icons':
specifier: 'catalog:'
version: 1.9.0(tailwindcss@4.1.11)
'@iconify-json/mingcute':
specifier: 'catalog:'
version: 1.2.3
'@tailwindcss/postcss':
specifier: 'catalog:'
version: 4.1.11
'@tailwindcss/typography':
specifier: 'catalog:'
version: 0.5.16(tailwindcss@4.1.11)
'@types/node':
specifier: 24.0.4
version: 24.0.4
@@ -154,6 +229,24 @@ importers:
next:
specifier: 15.3.4
version: 15.3.4(@babel/core@7.27.1)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
postcss:
specifier: 8.5.6
version: 8.5.6
tailwind-scrollbar:
specifier: 'catalog:'
version: 4.0.2(react@19.1.0)(tailwindcss@4.1.11)
tailwindcss:
specifier: 'catalog:'
version: 4.1.11
tailwindcss-animate:
specifier: 'catalog:'
version: 1.0.7(tailwindcss@4.1.11)
tailwindcss-safe-area:
specifier: 'catalog:'
version: 0.6.0(tailwindcss@4.1.11)
tailwindcss-uikit-colors:
specifier: 'catalog:'
version: 1.0.0-alpha.1
apps/ssr/sdk:
dependencies:
@@ -323,6 +416,9 @@ importers:
tailwind-merge:
specifier: 3.3.1
version: 3.3.1
tailwind-variants:
specifier: 1.0.0
version: 1.0.0(tailwindcss@4.1.11)
thumbhash:
specifier: 0.1.1
version: 0.1.1
@@ -343,19 +439,16 @@ importers:
version: 5.0.5(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))
devDependencies:
'@egoist/tailwindcss-icons':
specifier: 1.9.0
specifier: 'catalog:'
version: 1.9.0(tailwindcss@4.1.11)
'@iconify-json/mingcute':
specifier: 1.2.3
specifier: 'catalog:'
version: 1.2.3
'@tailwindcss/container-queries':
specifier: 0.1.1
version: 0.1.1(tailwindcss@4.1.11)
'@tailwindcss/postcss':
specifier: 4.1.11
specifier: 'catalog:'
version: 4.1.11
'@tailwindcss/typography':
specifier: 0.5.16
specifier: 'catalog:'
version: 0.5.16(tailwindcss@4.1.11)
'@tailwindcss/vite':
specifier: 4.1.11
@@ -387,6 +480,9 @@ importers:
execa:
specifier: 9.6.0
version: 9.6.0
kolorist:
specifier: 1.8.0
version: 1.8.0
postcss:
specifier: 8.5.6
version: 8.5.6
@@ -403,22 +499,19 @@ importers:
specifier: 2.13.0
version: 2.13.0
tailwind-scrollbar:
specifier: 4.0.2
specifier: 'catalog:'
version: 4.0.2(react@19.1.0)(tailwindcss@4.1.11)
tailwind-variants:
specifier: 1.0.0
version: 1.0.0(tailwindcss@4.1.11)
tailwindcss:
specifier: 4.1.11
specifier: 'catalog:'
version: 4.1.11
tailwindcss-animate:
specifier: 1.0.7
specifier: 'catalog:'
version: 1.0.7(tailwindcss@4.1.11)
tailwindcss-safe-area:
specifier: 0.6.0
specifier: 'catalog:'
version: 0.6.0(tailwindcss@4.1.11)
tailwindcss-uikit-colors:
specifier: 1.0.0-alpha.1
specifier: 'catalog:'
version: 1.0.0-alpha.1
unplugin-ast:
specifier: 0.15.0
@@ -463,6 +556,12 @@ importers:
specifier: 0.1.1
version: 0.1.1
packages/components:
dependencies:
react:
specifier: ^19
version: 19.1.0
packages/data:
dependencies:
'@afilmory/builder':
@@ -2926,11 +3025,6 @@ packages:
zod:
optional: true
'@tailwindcss/container-queries@0.1.1':
resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==}
peerDependencies:
tailwindcss: '>=3.2.0'
'@tailwindcss/node@4.1.11':
resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==}
@@ -3179,6 +3273,9 @@ packages:
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/js-cookie@2.2.7':
resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -3496,6 +3593,9 @@ packages:
'@vue/shared@3.5.16':
resolution: {integrity: sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==}
'@xobotyi/scrollbar-width@1.9.5':
resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -4042,12 +4142,19 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
css-in-js-utils@3.1.0:
resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
css-select@4.3.0:
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
css-select@5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
css-tree@1.1.3:
resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
engines: {node: '>=8.0.0'}
css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
@@ -4491,6 +4598,9 @@ packages:
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
error-stack-parser@2.1.4:
resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
es-toolkit@1.39.5:
resolution: {integrity: sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==}
@@ -4859,10 +4969,16 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-shallow-equal@1.0.0:
resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==}
fast-xml-parser@4.4.1:
resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==}
hasBin: true
fastest-stable-stringify@2.0.2:
resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==}
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
@@ -5177,6 +5293,9 @@ packages:
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
engines: {node: '>=18.18.0'}
hyphenate-style-name@1.1.0:
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
i18next-browser-languagedetector@8.2.0:
resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==}
@@ -5236,6 +5355,9 @@ packages:
inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
inline-style-prefixer@7.0.1:
resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==}
internmap@1.0.1:
resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==}
@@ -5367,6 +5489,9 @@ packages:
jpeg-js@0.4.4:
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
js-cookie@2.2.1:
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
@@ -5700,6 +5825,9 @@ packages:
mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
mdn-data@2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
meow@13.2.0:
resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
engines: {node: '>=18'}
@@ -5910,6 +6038,12 @@ packages:
resolution: {integrity: sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==}
deprecated: See https://github.com/mvdan/sh/issues/1145
nano-css@5.6.2:
resolution: {integrity: sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==}
peerDependencies:
react: '*'
react-dom: '*'
nano-spawn@1.0.2:
resolution: {integrity: sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==}
engines: {node: '>=20.17'}
@@ -6763,6 +6897,9 @@ packages:
'@types/react': '>=18'
react: '>=18'
react-masonry@1.0.7:
resolution: {integrity: sha512-cKhinTeygGtm+8y7YCTMKXNKtU0toJAcOBD4q7fv+JTc94bj93/xut3iZvjD60lZYek+WGM6en5w8P+63zHN9Q==}
react-merge-refs@3.0.2:
resolution: {integrity: sha512-MSZAfwFfdbEvwkKWP5EI5chuLYnNUxNS7vyS0i1Jp+wtd8J4Ga2ddzhaE68aMol2Z4vCnRM/oGOo1a3V75UPlw==}
peerDependencies:
@@ -6771,6 +6908,12 @@ packages:
react:
optional: true
react-photo-view@1.2.7:
resolution: {integrity: sha512-MfOWVPxuibncRLaycZUNxqYU8D9IA+rbGDDaq6GM8RIoGJal592hEJoRAyRSI7ZxyyJNJTLMUWWL3UIXHJJOpw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
@@ -6795,6 +6938,9 @@ packages:
'@types/react':
optional: true
react-responsive-masonry@2.7.1:
resolution: {integrity: sha512-Q+u+nOH87PzjqGFd2PgTcmLpHPZnCmUPREHYoNBc8dwJv6fi51p9U6hqwG8g/T8MN86HrFjrU+uQU6yvETU7cA==}
react-rnd@10.5.2:
resolution: {integrity: sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==}
peerDependencies:
@@ -6841,6 +6987,12 @@ packages:
'@types/react':
optional: true
react-universal-interface@0.6.2:
resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==}
peerDependencies:
react: '*'
tslib: '*'
react-use-measure@2.1.7:
resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
peerDependencies:
@@ -6850,6 +7002,12 @@ packages:
react-dom:
optional: true
react-use@17.6.0:
resolution: {integrity: sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==}
peerDependencies:
react: '*'
react-dom: '*'
react-zoom-pan-pinch@3.7.0:
resolution: {integrity: sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==}
engines: {node: '>=8', npm: '>=5'}
@@ -6857,6 +7015,10 @@ packages:
react: '*'
react-dom: '*'
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
react@19.1.0:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'}
@@ -7022,6 +7184,9 @@ packages:
roughjs@4.6.6:
resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==}
rtl-css-js@1.16.1:
resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -7071,6 +7236,10 @@ packages:
set-cookie-parser@2.7.1:
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
set-harmonic-interval@1.0.1:
resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==}
engines: {node: '>=6.9'}
set-value@2.0.1:
resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==}
engines: {node: '>=0.10.0'}
@@ -7143,6 +7312,10 @@ packages:
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.5.6:
resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==}
engines: {node: '>=0.10.0'}
source-map@0.5.7:
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
engines: {node: '>=0.10.0'}
@@ -7192,6 +7365,18 @@ packages:
stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
stack-generator@2.0.10:
resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==}
stackframe@1.3.4:
resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
stacktrace-gps@3.1.2:
resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==}
stacktrace-js@2.0.2:
resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==}
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
@@ -7357,6 +7542,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
throttle-debounce@3.0.1:
resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
engines: {node: '>=10'}
throttle-debounce@5.0.2:
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
engines: {node: '>=12.22'}
@@ -7429,6 +7618,9 @@ packages:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
engines: {node: '>=6.10'}
ts-easing@0.2.0:
resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==}
ts-md5@1.3.1:
resolution: {integrity: sha512-DiwiXfwvcTeZ5wCE0z+2A9EseZsztaiZtGrtSaY5JOD7ekPnR/GoIVD5gXZAlK9Na9Kvpo9Waz5rW64WKAWApg==}
engines: {node: '>=12'}
@@ -11041,10 +11233,6 @@ snapshots:
typescript: 5.8.3
zod: 3.25.67
'@tailwindcss/container-queries@0.1.1(tailwindcss@4.1.11)':
dependencies:
tailwindcss: 4.1.11
'@tailwindcss/node@4.1.11':
dependencies:
'@ampproject/remapping': 2.3.0
@@ -11318,6 +11506,8 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
'@types/js-cookie@2.2.7': {}
'@types/json-schema@7.0.15': {}
'@types/katex@0.16.7': {}
@@ -11681,6 +11871,8 @@ snapshots:
'@vue/shared@3.5.16': {}
'@xobotyi/scrollbar-width@1.9.5': {}
acorn-jsx@5.3.2(acorn@8.14.1):
dependencies:
acorn: 8.14.1
@@ -12283,6 +12475,10 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
css-in-js-utils@3.1.0:
dependencies:
hyphenate-style-name: 1.1.0
css-select@4.3.0:
dependencies:
boolbase: 1.0.0
@@ -12299,6 +12495,11 @@ snapshots:
domutils: 3.2.2
nth-check: 2.1.1
css-tree@1.1.3:
dependencies:
mdn-data: 2.0.14
source-map: 0.6.1
css-what@6.1.0: {}
cssesc@3.0.0: {}
@@ -12646,6 +12847,10 @@ snapshots:
dependencies:
is-arrayish: 0.2.1
error-stack-parser@2.1.4:
dependencies:
stackframe: 1.3.4
es-toolkit@1.39.5: {}
esast-util-from-estree@2.0.0:
@@ -13240,10 +13445,14 @@ snapshots:
fast-levenshtein@2.0.6: {}
fast-shallow-equal@1.0.0: {}
fast-xml-parser@4.4.1:
dependencies:
strnum: 1.1.2
fastest-stable-stringify@2.0.2: {}
fastq@1.19.1:
dependencies:
reusify: 1.1.0
@@ -13643,6 +13852,8 @@ snapshots:
human-signals@8.0.1: {}
hyphenate-style-name@1.1.0: {}
i18next-browser-languagedetector@8.2.0:
dependencies:
'@babel/runtime': 7.27.6
@@ -13687,6 +13898,10 @@ snapshots:
inline-style-parser@0.2.4: {}
inline-style-prefixer@7.0.1:
dependencies:
css-in-js-utils: 3.1.0
internmap@1.0.1: {}
internmap@2.0.3: {}
@@ -13784,6 +13999,8 @@ snapshots:
jpeg-js@0.4.4: {}
js-cookie@2.2.1: {}
js-cookie@3.0.5: {}
js-tokens@4.0.0: {}
@@ -14249,6 +14466,8 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
mdn-data@2.0.14: {}
meow@13.2.0: {}
merge-value@1.0.0:
@@ -14632,6 +14851,19 @@ snapshots:
mvdan-sh@0.10.1: {}
nano-css@5.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
css-tree: 1.1.3
csstype: 3.1.3
fastest-stable-stringify: 2.0.2
inline-style-prefixer: 7.0.1
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
rtl-css-js: 1.16.1
stacktrace-js: 2.0.2
stylis: 4.3.6
nano-spawn@1.0.2: {}
nanoid@3.3.11: {}
@@ -15540,10 +15772,19 @@ snapshots:
transitivePeerDependencies:
- supports-color
react-masonry@1.0.7:
dependencies:
react: 18.3.1
react-merge-refs@3.0.2(react@19.1.0):
optionalDependencies:
react: 19.1.0
react-photo-view@1.2.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-refresh@0.17.0: {}
react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.0):
@@ -15565,6 +15806,8 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.8
react-responsive-masonry@2.7.1: {}
react-rnd@10.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
re-resizable: 6.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -15619,17 +15862,45 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.8
react-universal-interface@0.6.2(react@19.1.0)(tslib@2.8.1):
dependencies:
react: 19.1.0
tslib: 2.8.1
react-use-measure@2.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
optionalDependencies:
react-dom: 19.1.0(react@19.1.0)
react-use@17.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@types/js-cookie': 2.2.7
'@xobotyi/scrollbar-width': 1.9.5
copy-to-clipboard: 3.3.3
fast-deep-equal: 3.1.3
fast-shallow-equal: 1.0.0
js-cookie: 2.2.1
nano-css: 5.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-universal-interface: 0.6.2(react@19.1.0)(tslib@2.8.1)
resize-observer-polyfill: 1.5.1
screenfull: 5.2.0
set-harmonic-interval: 1.0.1
throttle-debounce: 3.0.1
ts-easing: 0.2.0
tslib: 2.8.1
react-zoom-pan-pinch@3.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react@18.3.1:
dependencies:
loose-envify: 1.4.0
react@19.1.0: {}
read-cache@1.0.0:
@@ -15904,6 +16175,10 @@ snapshots:
points-on-curve: 0.2.0
points-on-path: 0.2.1
rtl-css-js@1.16.1:
dependencies:
'@babel/runtime': 7.27.6
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -15944,6 +16219,8 @@ snapshots:
set-cookie-parser@2.7.1: {}
set-harmonic-interval@1.0.1: {}
set-value@2.0.1:
dependencies:
extend-shallow: 2.0.1
@@ -16048,6 +16325,8 @@ snapshots:
buffer-from: 1.1.2
source-map: 0.6.1
source-map@0.5.6: {}
source-map@0.5.7: {}
source-map@0.6.1: {}
@@ -16084,6 +16363,23 @@ snapshots:
stable-hash@0.0.5: {}
stack-generator@2.0.10:
dependencies:
stackframe: 1.3.4
stackframe@1.3.4: {}
stacktrace-gps@3.1.2:
dependencies:
source-map: 0.5.6
stackframe: 1.3.4
stacktrace-js@2.0.2:
dependencies:
error-stack-parser: 2.1.4
stack-generator: 2.0.10
stacktrace-gps: 3.1.2
streamsearch@1.1.0: {}
string-argv@0.3.2: {}
@@ -16236,6 +16532,8 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
throttle-debounce@3.0.1: {}
throttle-debounce@5.0.2: {}
thumbhash@0.1.1: {}
@@ -16297,6 +16595,8 @@ snapshots:
ts-dedent@2.2.0: {}
ts-easing@0.2.0: {}
ts-md5@1.3.1: {}
ts-pattern@5.7.1: {}

View File

@@ -7,3 +7,13 @@ catalog:
'@t3-oss/env-core': 0.13.8
dotenv-expand: 12.0.2
typescript: 5.8.3
'tailwind-variants': '1.0.0'
'@tailwindcss/postcss': '4.1.11'
'@tailwindcss/typography': '0.5.16'
'tailwind-scrollbar': '4.0.2'
'tailwindcss': '4.1.11'
'tailwindcss-animate': '1.0.7'
'tailwindcss-safe-area': '0.6.0'
'tailwindcss-uikit-colors': '1.0.0-alpha.1'
'@egoist/tailwindcss-icons': '1.9.0'
'@iconify-json/mingcute': '1.2.3'