Initial work for postgen
This commit is contained in:
@@ -4,13 +4,13 @@
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "node --loader ts-node/esm ./src/bin.ts",
|
||||
"dev": "node --watch-path ./src --loader ts-node/esm ./src/bin.ts",
|
||||
"start": " NODE_ENV=production node --loader ts-node/esm ./src/bin.ts",
|
||||
"dev": "LOG_ENABLED=true NODE_ENV=development node --watch-path ./src --loader ts-node/esm ./src/bin.ts | pino-pretty",
|
||||
"build": "tsc && cp -r src/assets dist/",
|
||||
"install-fonts": "node --loader ts-node/esm scripts/install-fonts.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.12.19-next.0",
|
||||
"@atproto/api": "^0.13.4",
|
||||
"@atproto/common": "^0.4.0",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"express": "^4.19.2",
|
||||
@@ -18,10 +18,12 @@
|
||||
"pino": "^9.2.0",
|
||||
"react": "^18.3.1",
|
||||
"satori": "^0.10.13",
|
||||
"twemoji": "^14.0.2"
|
||||
"twemoji": "^14.0.2",
|
||||
"whatwg-mimetype": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.3",
|
||||
"pino-pretty": "^11.2.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
import {atoms as a, style as s} from '../theme/index.js'
|
||||
|
||||
export function Box({
|
||||
children,
|
||||
cx,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
cx?: Record<string, any>[]
|
||||
}) {
|
||||
return <div style={s([a.flex, a.flex_col, ...(cx ?? [])])}>{children}</div>
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react'
|
||||
|
||||
import {atoms as a} from '../theme/index.js'
|
||||
import {Box} from './Box.js'
|
||||
|
||||
export function Row({
|
||||
children,
|
||||
gutter = 0,
|
||||
cx,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
gutter?: number
|
||||
cx?: Record<string, any>[]
|
||||
}) {
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.flex_row,
|
||||
{
|
||||
marginLeft: -gutter,
|
||||
marginRight: -gutter,
|
||||
},
|
||||
...(cx || []),
|
||||
]}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function Column({
|
||||
children,
|
||||
gutter = 0,
|
||||
cx,
|
||||
width = 1,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
gutter?: number
|
||||
cx?: Record<string, any>[]
|
||||
width?: number
|
||||
}) {
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.flex_col,
|
||||
{
|
||||
paddingLeft: gutter,
|
||||
paddingRight: gutter,
|
||||
width: `${width * 100}%`,
|
||||
},
|
||||
...(cx || []),
|
||||
]}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
|
||||
import {style as s} from '../theme/index.js'
|
||||
import {Image as ImageSource} from '../util/resolvePostData.js'
|
||||
|
||||
export type ImageProps = Omit<
|
||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||
'src' | 'style'
|
||||
> & {
|
||||
image: ImageSource
|
||||
cx?: Record<string, any>[]
|
||||
}
|
||||
|
||||
export function Image({image, cx, ...rest}: ImageProps) {
|
||||
const {image: src, mime = 'image/jpeg'} = image
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
src={`data:${mime};base64,${src.toString('base64')}`}
|
||||
style={cx ? s(cx) : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable react-native-a11y/has-valid-accessibility-ignores-invert-colors */
|
||||
import React from 'react'
|
||||
|
||||
import {atoms as a, theme as t} from '../theme/index.js'
|
||||
import {Image as ImageSource} from '../util/resolvePostData.js'
|
||||
import {toShortUrl} from '../util/toShortUrl.js'
|
||||
import {Box} from './Box.js'
|
||||
import {Image} from './Image.js'
|
||||
import {Text} from './Text.js'
|
||||
|
||||
export function LinkCard({
|
||||
image,
|
||||
uri,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
image?: ImageSource
|
||||
uri?: string
|
||||
title: string
|
||||
description: string
|
||||
}) {
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.w_full,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
{image && (
|
||||
<Box
|
||||
cx={[
|
||||
a.relative,
|
||||
a.w_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
{paddingTop: (630 / 1200) * 100 + '%'},
|
||||
]}>
|
||||
<Image
|
||||
image={image}
|
||||
cx={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
{
|
||||
objectFit: 'cover',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box cx={[a.p_md, t.atoms.bg]}>
|
||||
{uri && (
|
||||
<Text cx={[a.text_xs, a.pb_sm, t.atoms.text_contrast_medium]}>
|
||||
{toShortUrl(uri)}
|
||||
</Text>
|
||||
)}
|
||||
<Text cx={[a.text_md, a.font_bold, a.pb_xs]}>{title}</Text>
|
||||
<Text cx={[a.text_sm, a.leading_snug]}>{description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
/* eslint-disable bsky-internal/avoid-unwrapped-text, react-native-a11y/has-valid-accessibility-ignores-invert-colors */
|
||||
import React from 'react'
|
||||
import {
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyGraphStarterpack,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {atoms as a, gradient, theme as t} from '../theme/index.js'
|
||||
import {formatCount} from '../util/formatCount.js'
|
||||
import {formatDate} from '../util/formatDate.js'
|
||||
import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js'
|
||||
import {Image as ImageSource, PostData} from '../util/resolvePostData.js'
|
||||
import {Box} from './Box.js'
|
||||
import * as Grid from './Grid.js'
|
||||
import {CircleInfo} from './icons/CircleInfo.js'
|
||||
import {Heart} from './icons/Heart.js'
|
||||
import {Logomark} from './icons/Logomark.js'
|
||||
import {Logotype} from './icons/Logotype.js'
|
||||
import {Repost} from './icons/Repost.js'
|
||||
import {Image} from './Image.js'
|
||||
import {LinkCard} from './LinkCard.js'
|
||||
import {RichText} from './RichText.js'
|
||||
import {Text} from './Text.js'
|
||||
|
||||
export function Post({
|
||||
post,
|
||||
data,
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
data: PostData
|
||||
}) {
|
||||
if (AppBskyFeedPost.isRecord(post.record)) {
|
||||
const avatar = data.images.get(post.author.avatar)
|
||||
const text = post.record.text
|
||||
const rt = data.texts.get(text)
|
||||
const hasInteractions = post.likeCount > 0 || post.repostCount > 0
|
||||
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.flex,
|
||||
a.align_center,
|
||||
a.w_full,
|
||||
a.h_full,
|
||||
a.p_3xl,
|
||||
{
|
||||
backgroundImage: `linear-gradient(to bottom, ${gradient('sky')})`,
|
||||
},
|
||||
]}>
|
||||
<Box
|
||||
cx={[a.flex, a.flex_col, a.w_full, a.p_xl, a.rounded_md, t.atoms.bg]}>
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_sm, a.pb_sm]}>
|
||||
<Box
|
||||
cx={[
|
||||
a.rounded_full,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
},
|
||||
]}>
|
||||
{avatar && <Image height="100%" width="100%" image={avatar} />}
|
||||
</Box>
|
||||
<Box cx={[a.flex_col]}>
|
||||
<Text cx={[a.text_md, a.font_bold, a.pb_2xs]}>
|
||||
{post.author.displayName || post.author.handle}
|
||||
</Text>
|
||||
<Text cx={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
@{post.author.handle}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{rt && <RichText value={rt} />}
|
||||
|
||||
{post.embed && (
|
||||
<Box cx={[a.pt_md]}>
|
||||
<Embeds embed={post.embed} data={data} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box cx={[a.flex_row, a.align_center, a.justify_between, a.pt_lg]}>
|
||||
<Text cx={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
{formatDate(post.record.createdAt)}
|
||||
</Text>
|
||||
|
||||
{!hasInteractions && <Logo />}
|
||||
</Box>
|
||||
|
||||
{hasInteractions && (
|
||||
<Box cx={[a.pt_md]}>
|
||||
<Box
|
||||
cx={[
|
||||
a.w_full,
|
||||
a.pb_md,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
]}
|
||||
/>
|
||||
|
||||
<Box cx={[a.flex_row, a.align_center, a.justify_between]}>
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_2xl]}>
|
||||
{post.likeCount > 0 && (
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Heart size={22} fill={t.palette.red_400} />
|
||||
<Box
|
||||
cx={[
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{formatCount(post.likeCount)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{post.repostCount > 0 && (
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Repost size={22} fill={t.palette.green_600} />
|
||||
<Box
|
||||
cx={[
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{formatCount(post.repostCount)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Logo />
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function Logo() {
|
||||
return (
|
||||
<Box cx={[a.flex_row, a.align_center]}>
|
||||
<Logomark size={20} fill={t.palette.primary_500} />
|
||||
<Box
|
||||
cx={[
|
||||
{
|
||||
paddingTop: '3px',
|
||||
paddingLeft: '6px',
|
||||
},
|
||||
]}>
|
||||
<Logotype size={64} fill={t.palette.contrast_800} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function Embeds({
|
||||
embed,
|
||||
data,
|
||||
hideNestedEmbeds,
|
||||
}: {
|
||||
embed: AppBskyFeedDefs.PostView['embed']
|
||||
data: PostData
|
||||
hideNestedEmbeds?: boolean
|
||||
}) {
|
||||
if (AppBskyEmbedExternal.isView(embed)) {
|
||||
const {title, description, uri, thumb} = embed.external
|
||||
const image = data.images.get(thumb)
|
||||
return (
|
||||
<LinkCard
|
||||
image={image}
|
||||
title={title}
|
||||
description={description}
|
||||
uri={uri}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (AppBskyEmbedImages.isView(embed)) {
|
||||
const {images} = embed
|
||||
|
||||
if (images.length > 0) {
|
||||
const imgs = images.map(({fullsize}) => data.images.get(fullsize))
|
||||
if (imgs.length === 1) {
|
||||
return (
|
||||
<Box cx={[a.rounded_sm, a.w_full, a.overflow_hidden]}>
|
||||
<Image image={imgs[0]} />
|
||||
</Box>
|
||||
)
|
||||
} else if (imgs.length === 2) {
|
||||
return (
|
||||
<Grid.Row gutter={a.p_xs.padding}>
|
||||
<Grid.Column gutter={a.p_xs.padding} width={1 / 2}>
|
||||
<SquareImage image={imgs[0]} />
|
||||
</Grid.Column>
|
||||
<Grid.Column gutter={a.p_xs.padding} width={1 / 2}>
|
||||
<SquareImage image={imgs[1]} />
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
)
|
||||
} else if (imgs.length === 3) {
|
||||
return (
|
||||
<Grid.Row gutter={a.p_xs.padding}>
|
||||
<Grid.Column gutter={a.p_xs.padding} width={2 / 3}>
|
||||
<SquareImage image={imgs[0]} />
|
||||
</Grid.Column>
|
||||
<Grid.Column gutter={a.p_xs.padding} width={1 / 3} cx={[a.gap_sm]}>
|
||||
<SquareImage image={imgs[1]} />
|
||||
<SquareImage image={imgs[2]} />
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Grid.Row gutter={a.p_xs.padding}>
|
||||
<Grid.Column gutter={a.p_xs.padding} width={1 / 2} cx={[a.gap_sm]}>
|
||||
<SquareImage image={imgs[0]} />
|
||||
<SquareImage image={imgs[1]} />
|
||||
</Grid.Column>
|
||||
<Grid.Column gutter={a.p_xs.padding} width={1 / 2} cx={[a.gap_sm]}>
|
||||
<SquareImage image={imgs[2]} />
|
||||
<SquareImage image={imgs[3]} />
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyEmbedRecord.isView(embed)) {
|
||||
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
|
||||
return <FeedCard embed={embed.record} data={data} />
|
||||
}
|
||||
if (AppBskyGraphDefs.isListView(embed.record)) {
|
||||
return <ListCard embed={embed.record} data={data} />
|
||||
}
|
||||
if (
|
||||
AppBskyGraphDefs.isStarterPackViewBasic(embed.record) &&
|
||||
AppBskyGraphStarterpack.isRecord(embed.record.record)
|
||||
) {
|
||||
const uri = getStarterPackImageUri(embed.record)
|
||||
const {name, description} = embed.record.record
|
||||
const image = data.images.get(uri)
|
||||
return <LinkCard image={image} title={name} description={description} />
|
||||
}
|
||||
return <QuoteEmbed embed={embed} data={data} />
|
||||
}
|
||||
|
||||
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||
return (
|
||||
<Box cx={[a.gap_md]}>
|
||||
<Embeds embed={embed.media} data={data} />
|
||||
{!hideNestedEmbeds && <QuoteEmbed embed={embed.record} data={data} />}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function FeedCard({
|
||||
embed,
|
||||
data,
|
||||
}: {
|
||||
embed: AppBskyFeedDefs.GeneratorView
|
||||
data: PostData
|
||||
}) {
|
||||
const {avatar, displayName, likeCount, creator} = embed
|
||||
const image = data.images.get(avatar)
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.w_full,
|
||||
a.gap_sm,
|
||||
a.rounded_sm,
|
||||
a.p_md,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
{image && (
|
||||
<Image
|
||||
image={image}
|
||||
cx={[
|
||||
a.rounded_sm,
|
||||
{
|
||||
width: '40px',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Box cx={[a.pt_2xs]}>
|
||||
<Text cx={[a.text_md, a.font_bold, a.pb_2xs]}>{displayName}</Text>
|
||||
<Text cx={[a.text_sm, a.leading_snug]}>By @{creator.handle}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{likeCount > 1 && (
|
||||
<Text cx={[a.text_sm, a.leading_snug]}>
|
||||
Liked by {formatCount(likeCount)} users
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function ListCard({
|
||||
embed,
|
||||
data,
|
||||
}: {
|
||||
embed: AppBskyGraphDefs.ListView
|
||||
data: PostData
|
||||
}) {
|
||||
const {avatar, name, creator} = embed
|
||||
const image = data.images.get(avatar)
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.w_full,
|
||||
a.gap_sm,
|
||||
a.rounded_sm,
|
||||
a.p_md,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
{image && (
|
||||
<Image
|
||||
image={image}
|
||||
cx={[
|
||||
a.rounded_sm,
|
||||
{
|
||||
width: '40px',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Box cx={[a.pt_2xs]}>
|
||||
<Text cx={[a.text_md, a.font_bold, a.pb_2xs]}>{name}</Text>
|
||||
<Text cx={[a.text_sm, a.leading_snug]}>By @{creator.handle}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function SquareImage({image}: {image: ImageSource}) {
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.relative,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
a.w_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
{paddingTop: '100%'},
|
||||
]}>
|
||||
<Image
|
||||
image={image}
|
||||
cx={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
{
|
||||
objectFit: 'cover',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function QuoteEmbed({
|
||||
embed,
|
||||
data,
|
||||
}: {
|
||||
embed: AppBskyEmbedRecord.View
|
||||
data: PostData
|
||||
}) {
|
||||
if (
|
||||
AppBskyEmbedRecord.isViewRecord(embed.record) &&
|
||||
AppBskyFeedPost.isRecord(embed.record.value) &&
|
||||
AppBskyFeedPost.validateRecord(embed.record.value).success
|
||||
) {
|
||||
const {author, value: post, embeds} = embed.record
|
||||
const avatar = data.images.get(author.avatar)
|
||||
const rt = data.texts.get(post.text)
|
||||
const notPublic = author.labels.some(l => l.val === `!no-unauthenticated`)
|
||||
|
||||
if (notPublic) {
|
||||
return (
|
||||
<NotQuoteEmbed>
|
||||
The author of the quoted post has requested their posts not be
|
||||
displayed on external sites
|
||||
</NotQuoteEmbed>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.w_full,
|
||||
a.p_md,
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_xs, a.pb_sm]}>
|
||||
<Box
|
||||
cx={[
|
||||
a.rounded_full,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
},
|
||||
]}>
|
||||
{avatar && <Image height="100%" width="100%" image={avatar} />}
|
||||
</Box>
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<Text cx={[a.text_sm, a.font_bold]}>
|
||||
{author.displayName || author.handle}
|
||||
</Text>
|
||||
<Text cx={[a.text_xs, t.atoms.text_contrast_medium]}>
|
||||
@{author.handle}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{rt && (
|
||||
<Box>
|
||||
<RichText value={rt} cx={[a.text_sm]} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{Boolean(embeds && embeds.length) && (
|
||||
<Box cx={[a.pt_sm]}>
|
||||
<Embeds embed={embeds[0]} data={data} hideNestedEmbeds />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
} else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) {
|
||||
return <NotQuoteEmbed>Quoted post is blocked</NotQuoteEmbed>
|
||||
} else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) {
|
||||
return (
|
||||
<NotQuoteEmbed>
|
||||
Quoted post not found, it may have been deleted.
|
||||
</NotQuoteEmbed>
|
||||
)
|
||||
} else if (AppBskyEmbedRecord.isViewDetached(embed.record)) {
|
||||
return <NotQuoteEmbed>Quoted post detached by author</NotQuoteEmbed>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function NotQuoteEmbed({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
a.rounded_sm,
|
||||
a.p_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<CircleInfo size={20} fill={t.atoms.text_contrast_low.color} />
|
||||
<Text cx={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
{children}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react'
|
||||
import {AppBskyRichtextFacet, RichText as RichTextApi} from '@atproto/api'
|
||||
|
||||
import {atoms as a, style as s, theme as t} from '../theme/index.js'
|
||||
import {toShortUrl} from '../util/toShortUrl.js'
|
||||
import {Text} from './Text.js'
|
||||
|
||||
export function RichText({
|
||||
value,
|
||||
disableLinks,
|
||||
cx,
|
||||
}: {
|
||||
value: RichTextApi
|
||||
disableLinks?: boolean
|
||||
cx?: Record<string, any>[]
|
||||
}) {
|
||||
const {facets} = value
|
||||
const baseStyles = [
|
||||
a.leading_snug,
|
||||
{
|
||||
whiteSpace: 'pre-wrap',
|
||||
},
|
||||
cx ? s(cx) : {},
|
||||
]
|
||||
const linkStyles = [
|
||||
{
|
||||
color: t.palette.primary_500,
|
||||
},
|
||||
]
|
||||
|
||||
if (!facets?.length) {
|
||||
// if (isOnlyEmoji(text)) {
|
||||
// const fontSize =
|
||||
// (flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
|
||||
// return (
|
||||
// <Text
|
||||
// selectable={selectable}
|
||||
// testID={testID}
|
||||
// style={[baseStyles, {fontSize}]}
|
||||
// // @ts-ignore web only -prf
|
||||
// dataSet={WORD_WRAP}>
|
||||
// {text}
|
||||
// </Text>
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
const els = []
|
||||
let key = 0
|
||||
// N.B. must access segments via `richText.segments`, not via destructuring
|
||||
for (const segment of value.segments()) {
|
||||
const {text, link, mention, tag} = segment
|
||||
if (
|
||||
mention &&
|
||||
AppBskyRichtextFacet.validateMention(mention).success &&
|
||||
!disableLinks
|
||||
) {
|
||||
els.push(
|
||||
<span key={key} style={s(linkStyles)}>
|
||||
{text}
|
||||
</span>,
|
||||
)
|
||||
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
|
||||
const url = toShortUrl(text)
|
||||
if (disableLinks) {
|
||||
els.push(url)
|
||||
} else {
|
||||
els.push(
|
||||
<span key={key} style={s(linkStyles)}>
|
||||
{url}
|
||||
</span>,
|
||||
)
|
||||
}
|
||||
} else if (
|
||||
!disableLinks &&
|
||||
tag &&
|
||||
AppBskyRichtextFacet.validateTag(tag).success
|
||||
) {
|
||||
els.push(
|
||||
<span key={key} style={s(linkStyles)}>
|
||||
{segment.text}
|
||||
</span>,
|
||||
)
|
||||
} else {
|
||||
els.push(segment.text)
|
||||
}
|
||||
key++
|
||||
}
|
||||
|
||||
return <Text cx={baseStyles}>{els}</Text>
|
||||
}
|
||||
@@ -106,6 +106,7 @@ export function StarterPack(props: {
|
||||
color: 'white',
|
||||
padding: 60,
|
||||
fontSize: 40,
|
||||
fontWeight: '700',
|
||||
}}>
|
||||
JOIN THE CONVERSATION
|
||||
</div>
|
||||
@@ -134,6 +135,7 @@ export function StarterPack(props: {
|
||||
fontSize: isLongTitle ? 55 : 65,
|
||||
display: 'flex',
|
||||
textAlign: 'center',
|
||||
fontWeight: '700',
|
||||
}}>
|
||||
{record?.name || 'Starter Pack'}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
|
||||
import {atoms as a, style as s, theme as t} from '../theme/index.js'
|
||||
|
||||
export function Text({
|
||||
children,
|
||||
cx,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
cx?: Record<string, any>[]
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={s([
|
||||
a.flex,
|
||||
a.flex_col,
|
||||
a.font_normal,
|
||||
a.leading_tight,
|
||||
a.tracking_wide,
|
||||
t.atoms.text,
|
||||
...(cx ?? []),
|
||||
])}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
export function CircleInfo({size, fill}: {size: number; fill?: string}) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-1a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-4a1 1 0 0 1-1-1Zm1-3a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
export function Heart({size, fill}: {size: number; fill?: string}) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.489 21.372c8.528-4.78 10.626-10.47 9.022-14.47-.779-1.941-2.414-3.333-4.342-3.763-1.697-.378-3.552.003-5.169 1.287-1.617-1.284-3.472-1.665-5.17-1.287-1.927.43-3.562 1.822-4.34 3.764-1.605 4 .493 9.69 9.021 14.47a1 1 0 0 0 .978 0Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
export function Logomark({size, fill}: {size: number; fill?: string}) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 500 441" width={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
d="M108.382 29.634C165.705 72.668 227.36 159.93 250 206.754c22.64-46.824 84.295-134.084 141.618-177.12C432.98-1.42 500-25.447 500 51.008c0 15.269-8.755 128.269-13.889 146.615-17.848 63.779-82.883 80.047-140.735 70.2 101.122 17.211 126.846 74.218 71.291 131.225-105.511 108.268-151.649-27.165-163.471-61.867-2.167-6.361-3.181-9.338-3.196-6.807-.015-2.531-1.029.446-3.196 6.807-11.822 34.702-57.96 170.135-163.47 61.867-55.556-57.007-29.832-114.014 71.29-131.225-57.852 9.847-122.887-6.421-140.735-70.2C8.755 179.278 0 66.278 0 51.009 0-25.446 67.02-1.419 108.382 29.634Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
export function Logotype({size, fill}: {size: number; fill?: string}) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 398 108" width={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
d="M53.139 40.276c9.306 3.396 14.247 11.24 14.247 20.022 0 14.87-9.766 23.886-28.494 23.886H.632V.818h36.996c17.809 0 26.426 9.25 26.426 21.544 0 8.196-3.677 14.167-10.915 17.914Zm-16.66-26.462H16.143v21.779h20.336c7.928 0 12.179-4.215 12.179-11.24 0-6.44-4.366-10.539-12.179-10.539ZM16.143 71.07h21.945c8.732 0 13.442-4.098 13.442-11.474 0-7.728-4.48-11.592-13.442-11.592H16.143V71.07ZM88.892 84.184H74.415V.818h14.477v83.366ZM136.892 57.605V23.767h14.477v60.417h-14.017v-8.782c-4.481 6.791-10.686 10.187-18.613 10.187-12.524 0-20.681-7.728-20.681-21.778V23.767h14.476v37.585c0 7.61 3.677 11.474 11.145 11.474 7.009 0 13.213-5.268 13.213-15.22ZM217.264 55.03v3.512h-44.35c1.034 10.42 6.664 15.572 15.396 15.572 6.664 0 11.145-2.927 13.558-8.664h13.902c-3.102 12.294-13.443 20.139-27.575 20.139-8.847 0-15.97-2.927-21.37-8.665-5.4-5.737-8.158-13.347-8.158-22.949 0-9.484 2.643-17.094 8.043-22.949 5.4-5.737 12.409-8.664 21.256-8.664 8.961 0 16.085 3.044 21.37 9.016 5.285 5.971 7.928 13.933 7.928 23.651Zm-29.413-21.194c-7.928 0-13.443 4.684-14.822 14.402h29.758c-1.264-8.781-6.549-14.402-14.936-14.402ZM249.35 85.823c-17.234 0-26.311-6.908-27.115-20.841h14.132c.804 7.493 4.481 10.303 13.213 10.303 7.813 0 11.719-2.459 11.719-7.26 0-4.331-2.757-6.439-11.604-7.961l-6.779-1.17c-12.983-2.226-19.417-8.314-19.417-18.267 0-11.357 8.847-18.265 24.587-18.265 16.89 0 25.622 6.79 26.196 20.49H260.61c-.345-7.376-4.596-9.952-12.524-9.952-6.894 0-10.34 2.342-10.34 7.025 0 4.215 2.987 6.089 9.881 7.376l7.468 1.171c14.362 2.693 20.566 8.08 20.566 18.383 0 12.177-9.651 18.968-26.311 18.968ZM339.201 84.184h-16.545l-17.235-28.101-8.961 9.133v18.968h-14.247V.818h14.247v48.006l24.128-25.057h17.234l-22.405 22.832 23.784 37.585ZM378.008 38.988l4.826-15.221H398L375.136 89.1c-2.413 6.674-5.4 11.475-9.192 14.168-3.791 2.693-9.191 3.981-16.315 3.981-2.413 0-4.481-.117-6.319-.351V95.307h5.515c6.549 0 9.766-4.098 9.766-9.718 0-2.81-.919-6.908-2.758-12.177l-17.234-49.645h15.626l4.825 15.104c3.562 11.358 6.664 22.598 9.422 33.721 2.528-9.6 5.745-20.841 9.536-33.604Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
export function Repost({size, fill}: {size: number; fill?: string}) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.957 2.293a1 1 0 1 0-1.414 1.414L17.836 5H6a3 3 0 0 0-3 3v3a1 1 0 1 0 2 0V8a1 1 0 0 1 1-1h11.836l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.47-2.47a1.75 1.75 0 0 0 0-2.474l-2.47-2.47ZM20 12a1 1 0 0 1 1 1v3a3 3 0 0 1-3 3H6.164l1.293 1.293a1 1 0 1 1-1.414 1.414l-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47a1 1 0 0 1 1.414 1.414L6.164 17H18a1 1 0 0 0 1-1v-3a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -31,9 +31,12 @@ export class AppContext {
|
||||
const fontDirectory = path.join(__DIRNAME, 'assets', 'fonts')
|
||||
const fontFiles = readdirSync(fontDirectory)
|
||||
const fonts = fontFiles.map(file => {
|
||||
const basename = path.basename(file, path.extname(file))
|
||||
const [weight, name] = basename.split('-')
|
||||
return {
|
||||
name: path.basename(file, path.extname(file)),
|
||||
name,
|
||||
data: readFileSync(path.join(fontDirectory, file)),
|
||||
weight: parseInt(weight),
|
||||
}
|
||||
})
|
||||
return new AppContext({
|
||||
|
||||
@@ -2,6 +2,7 @@ import {Express} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {default as health} from './health.js'
|
||||
import {default as post} from './post.js'
|
||||
import {default as starterPack} from './starter-pack.js'
|
||||
|
||||
export * from './util.js'
|
||||
@@ -9,5 +10,6 @@ export * from './util.js'
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
app = health(ctx, app) // GET /_health
|
||||
app = starterPack(ctx, app) // GET /start/:actor/:rkey
|
||||
app = post(ctx, app) // POST /post
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from 'react'
|
||||
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
|
||||
import resvg from '@resvg/resvg-js'
|
||||
import {Express} from 'express'
|
||||
import satori from 'satori'
|
||||
|
||||
import {Post} from '../components/Post.js'
|
||||
import {AppContext} from '../context.js'
|
||||
import {httpLogger} from '../logger.js'
|
||||
import {loadEmojiAsSvg} from '../util.js'
|
||||
import {resolvePostData} from '../util/resolvePostData.js'
|
||||
import {handler, originVerifyMiddleware} from './util.js'
|
||||
|
||||
const WIDTH = 600
|
||||
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
return app.get(
|
||||
'/:actor/post/:rkey',
|
||||
originVerifyMiddleware(ctx),
|
||||
handler(async (req, res) => {
|
||||
let {actor, rkey} = req.params
|
||||
let uri = AtUri.make(actor, 'app.bsky.feed.post', rkey)
|
||||
|
||||
let post: AppBskyFeedDefs.PostView
|
||||
|
||||
try {
|
||||
if (!actor.startsWith('did:')) {
|
||||
const res = await ctx.appviewAgent.resolveHandle({
|
||||
handle: actor,
|
||||
})
|
||||
actor = res.data.did
|
||||
}
|
||||
uri = AtUri.make(actor, 'app.bsky.feed.post', rkey)
|
||||
const {data} = await ctx.appviewAgent.getPosts({
|
||||
uris: [uri.toString()],
|
||||
})
|
||||
post = data.posts.at(0)
|
||||
|
||||
const notPublic = post.author.labels.some(
|
||||
l => l.val === `!no-unauthenticated`,
|
||||
)
|
||||
if (notPublic) {
|
||||
return res.status(404).end('not found')
|
||||
}
|
||||
} catch (err) {
|
||||
httpLogger.warn({err, uri: uri.toString()}, 'could not fetch post')
|
||||
return res.status(404).end('not found')
|
||||
}
|
||||
|
||||
if (!AppBskyFeedPost.isRecord(post.record)) {
|
||||
return res.status(404).end('not found')
|
||||
}
|
||||
|
||||
const data = await resolvePostData(post, ctx.appviewAgent)
|
||||
const svg = await satori(<Post post={post} data={data} />, {
|
||||
fonts: ctx.fonts,
|
||||
width: WIDTH,
|
||||
loadAdditionalAsset: async (code, text) => {
|
||||
if (code === 'emoji') {
|
||||
return await loadEmojiAsSvg(text)
|
||||
}
|
||||
},
|
||||
})
|
||||
const output = await resvg.renderAsync(svg, {
|
||||
fitTo: {
|
||||
mode: 'width',
|
||||
value: WIDTH * 2,
|
||||
},
|
||||
logLevel: 'trace',
|
||||
})
|
||||
res.statusCode = 200
|
||||
res.setHeader('content-type', 'image/png')
|
||||
res.setHeader('cdn-tag', [...data.images.keys()].join(','))
|
||||
return res.end(output.asPng())
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
import * as tokens from './tokens.js'
|
||||
|
||||
export const atoms = {
|
||||
block: {
|
||||
display: 'block',
|
||||
},
|
||||
inline_block: {
|
||||
display: 'inline-block',
|
||||
},
|
||||
inline: {
|
||||
display: 'inline',
|
||||
},
|
||||
/*
|
||||
* Positioning
|
||||
*/
|
||||
fixed: {
|
||||
position: 'absolute',
|
||||
},
|
||||
absolute: {
|
||||
position: 'absolute',
|
||||
},
|
||||
relative: {
|
||||
position: 'relative',
|
||||
},
|
||||
inset_0: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
|
||||
overflow_hidden: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
/*
|
||||
* Width
|
||||
*/
|
||||
w_full: {
|
||||
width: '100%',
|
||||
},
|
||||
h_full: {
|
||||
height: '100%',
|
||||
},
|
||||
h_full_vh: {
|
||||
height: '100vh',
|
||||
},
|
||||
|
||||
/*
|
||||
* Theme-independent bg colors
|
||||
*/
|
||||
bg_transparent: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
|
||||
/*
|
||||
* Border radius
|
||||
*/
|
||||
rounded_2xs: {
|
||||
borderRadius: tokens.borderRadius._2xs,
|
||||
},
|
||||
rounded_xs: {
|
||||
borderRadius: tokens.borderRadius.xs,
|
||||
},
|
||||
rounded_sm: {
|
||||
borderRadius: tokens.borderRadius.sm,
|
||||
},
|
||||
rounded_md: {
|
||||
borderRadius: tokens.borderRadius.md,
|
||||
},
|
||||
rounded_full: {
|
||||
borderRadius: tokens.borderRadius.full,
|
||||
},
|
||||
|
||||
/*
|
||||
* Flex
|
||||
*/
|
||||
gap_2xs: {
|
||||
gap: tokens.space._2xs,
|
||||
},
|
||||
gap_xs: {
|
||||
gap: tokens.space.xs,
|
||||
},
|
||||
gap_sm: {
|
||||
gap: tokens.space.sm,
|
||||
},
|
||||
gap_md: {
|
||||
gap: tokens.space.md,
|
||||
},
|
||||
gap_lg: {
|
||||
gap: tokens.space.lg,
|
||||
},
|
||||
gap_xl: {
|
||||
gap: tokens.space.xl,
|
||||
},
|
||||
gap_2xl: {
|
||||
gap: tokens.space._2xl,
|
||||
},
|
||||
gap_3xl: {
|
||||
gap: tokens.space._3xl,
|
||||
},
|
||||
gap_4xl: {
|
||||
gap: tokens.space._4xl,
|
||||
},
|
||||
gap_5xl: {
|
||||
gap: tokens.space._5xl,
|
||||
},
|
||||
flex: {
|
||||
display: 'flex',
|
||||
},
|
||||
flex_col: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
flex_row: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
flex_col_reverse: {
|
||||
flexDirection: 'column-reverse',
|
||||
},
|
||||
flex_row_reverse: {
|
||||
flexDirection: 'row-reverse',
|
||||
},
|
||||
flex_wrap: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
flex_0: {
|
||||
flex: '0 0 auto',
|
||||
},
|
||||
flex_1: {
|
||||
flex: 1,
|
||||
},
|
||||
flex_grow: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
flex_shrink: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
flex_shrink_0: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
justify_start: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
justify_center: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
justify_between: {
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
justify_end: {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
align_center: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
align_start: {
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
align_end: {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
align_baseline: {
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
align_stretch: {
|
||||
alignItems: 'stretch',
|
||||
},
|
||||
self_auto: {
|
||||
alignSelf: 'auto',
|
||||
},
|
||||
self_start: {
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
self_end: {
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
self_center: {
|
||||
alignSelf: 'center',
|
||||
},
|
||||
self_stretch: {
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
self_baseline: {
|
||||
alignSelf: 'baseline',
|
||||
},
|
||||
|
||||
/*
|
||||
* Text
|
||||
*/
|
||||
text_left: {
|
||||
textAlign: 'left',
|
||||
},
|
||||
text_center: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
text_right: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
text_2xs: {
|
||||
fontSize: tokens.fontSize._2xs,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_xs: {
|
||||
fontSize: tokens.fontSize.xs,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_sm: {
|
||||
fontSize: tokens.fontSize.sm,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_md: {
|
||||
fontSize: tokens.fontSize.md,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_lg: {
|
||||
fontSize: tokens.fontSize.lg,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_xl: {
|
||||
fontSize: tokens.fontSize.xl,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_2xl: {
|
||||
fontSize: tokens.fontSize._2xl,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_3xl: {
|
||||
fontSize: tokens.fontSize._3xl,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_4xl: {
|
||||
fontSize: tokens.fontSize._4xl,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_5xl: {
|
||||
fontSize: tokens.fontSize._5xl,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
leading_tight: {
|
||||
lineHeight: '1.15',
|
||||
},
|
||||
leading_snug: {
|
||||
lineHeight: '1.3',
|
||||
},
|
||||
leading_normal: {
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
tracking_normal: {
|
||||
letterSpacing: 0,
|
||||
},
|
||||
tracking_wide: {
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
font_normal: {
|
||||
fontWeight: tokens.fontWeight.normal,
|
||||
},
|
||||
font_semibold: {
|
||||
fontWeight: tokens.fontWeight.semibold,
|
||||
},
|
||||
font_bold: {
|
||||
fontWeight: tokens.fontWeight.bold,
|
||||
},
|
||||
font_heavy: {
|
||||
fontWeight: tokens.fontWeight.heavy,
|
||||
},
|
||||
italic: {
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
mono: {
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
|
||||
/*
|
||||
* Border
|
||||
*/
|
||||
border_0: {
|
||||
borderWidth: '0px',
|
||||
},
|
||||
border: {
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
},
|
||||
border_t: {
|
||||
borderTopWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
},
|
||||
border_b: {
|
||||
borderBottomWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
},
|
||||
border_l: {
|
||||
borderLeftWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
},
|
||||
border_r: {
|
||||
borderRightWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
},
|
||||
|
||||
/*
|
||||
* Padding
|
||||
*/
|
||||
p_0: {
|
||||
padding: 0,
|
||||
},
|
||||
p_2xs: {
|
||||
padding: tokens.space._2xs,
|
||||
},
|
||||
p_xs: {
|
||||
padding: tokens.space.xs,
|
||||
},
|
||||
p_sm: {
|
||||
padding: tokens.space.sm,
|
||||
},
|
||||
p_md: {
|
||||
padding: tokens.space.md,
|
||||
},
|
||||
p_lg: {
|
||||
padding: tokens.space.lg,
|
||||
},
|
||||
p_xl: {
|
||||
padding: tokens.space.xl,
|
||||
},
|
||||
p_2xl: {
|
||||
padding: tokens.space._2xl,
|
||||
},
|
||||
p_3xl: {
|
||||
padding: tokens.space._3xl,
|
||||
},
|
||||
p_4xl: {
|
||||
padding: tokens.space._4xl,
|
||||
},
|
||||
p_5xl: {
|
||||
padding: tokens.space._5xl,
|
||||
},
|
||||
px_0: {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
},
|
||||
px_2xs: {
|
||||
paddingLeft: tokens.space._2xs,
|
||||
paddingRight: tokens.space._2xs,
|
||||
},
|
||||
px_xs: {
|
||||
paddingLeft: tokens.space.xs,
|
||||
paddingRight: tokens.space.xs,
|
||||
},
|
||||
px_sm: {
|
||||
paddingLeft: tokens.space.sm,
|
||||
paddingRight: tokens.space.sm,
|
||||
},
|
||||
px_md: {
|
||||
paddingLeft: tokens.space.md,
|
||||
paddingRight: tokens.space.md,
|
||||
},
|
||||
px_lg: {
|
||||
paddingLeft: tokens.space.lg,
|
||||
paddingRight: tokens.space.lg,
|
||||
},
|
||||
px_xl: {
|
||||
paddingLeft: tokens.space.xl,
|
||||
paddingRight: tokens.space.xl,
|
||||
},
|
||||
px_2xl: {
|
||||
paddingLeft: tokens.space._2xl,
|
||||
paddingRight: tokens.space._2xl,
|
||||
},
|
||||
px_3xl: {
|
||||
paddingLeft: tokens.space._3xl,
|
||||
paddingRight: tokens.space._3xl,
|
||||
},
|
||||
px_4xl: {
|
||||
paddingLeft: tokens.space._4xl,
|
||||
paddingRight: tokens.space._4xl,
|
||||
},
|
||||
px_5xl: {
|
||||
paddingLeft: tokens.space._5xl,
|
||||
paddingRight: tokens.space._5xl,
|
||||
},
|
||||
py_0: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
py_2xs: {
|
||||
paddingTop: tokens.space._2xs,
|
||||
paddingBottom: tokens.space._2xs,
|
||||
},
|
||||
py_xs: {
|
||||
paddingTop: tokens.space.xs,
|
||||
paddingBottom: tokens.space.xs,
|
||||
},
|
||||
py_sm: {
|
||||
paddingTop: tokens.space.sm,
|
||||
paddingBottom: tokens.space.sm,
|
||||
},
|
||||
py_md: {
|
||||
paddingTop: tokens.space.md,
|
||||
paddingBottom: tokens.space.md,
|
||||
},
|
||||
py_lg: {
|
||||
paddingTop: tokens.space.lg,
|
||||
paddingBottom: tokens.space.lg,
|
||||
},
|
||||
py_xl: {
|
||||
paddingTop: tokens.space.xl,
|
||||
paddingBottom: tokens.space.xl,
|
||||
},
|
||||
py_2xl: {
|
||||
paddingTop: tokens.space._2xl,
|
||||
paddingBottom: tokens.space._2xl,
|
||||
},
|
||||
py_3xl: {
|
||||
paddingTop: tokens.space._3xl,
|
||||
paddingBottom: tokens.space._3xl,
|
||||
},
|
||||
py_4xl: {
|
||||
paddingTop: tokens.space._4xl,
|
||||
paddingBottom: tokens.space._4xl,
|
||||
},
|
||||
py_5xl: {
|
||||
paddingTop: tokens.space._5xl,
|
||||
paddingBottom: tokens.space._5xl,
|
||||
},
|
||||
pt_0: {
|
||||
paddingTop: 0,
|
||||
},
|
||||
pt_2xs: {
|
||||
paddingTop: tokens.space._2xs,
|
||||
},
|
||||
pt_xs: {
|
||||
paddingTop: tokens.space.xs,
|
||||
},
|
||||
pt_sm: {
|
||||
paddingTop: tokens.space.sm,
|
||||
},
|
||||
pt_md: {
|
||||
paddingTop: tokens.space.md,
|
||||
},
|
||||
pt_lg: {
|
||||
paddingTop: tokens.space.lg,
|
||||
},
|
||||
pt_xl: {
|
||||
paddingTop: tokens.space.xl,
|
||||
},
|
||||
pt_2xl: {
|
||||
paddingTop: tokens.space._2xl,
|
||||
},
|
||||
pt_3xl: {
|
||||
paddingTop: tokens.space._3xl,
|
||||
},
|
||||
pt_4xl: {
|
||||
paddingTop: tokens.space._4xl,
|
||||
},
|
||||
pt_5xl: {
|
||||
paddingTop: tokens.space._5xl,
|
||||
},
|
||||
pb_0: {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
pb_2xs: {
|
||||
paddingBottom: tokens.space._2xs,
|
||||
},
|
||||
pb_xs: {
|
||||
paddingBottom: tokens.space.xs,
|
||||
},
|
||||
pb_sm: {
|
||||
paddingBottom: tokens.space.sm,
|
||||
},
|
||||
pb_md: {
|
||||
paddingBottom: tokens.space.md,
|
||||
},
|
||||
pb_lg: {
|
||||
paddingBottom: tokens.space.lg,
|
||||
},
|
||||
pb_xl: {
|
||||
paddingBottom: tokens.space.xl,
|
||||
},
|
||||
pb_2xl: {
|
||||
paddingBottom: tokens.space._2xl,
|
||||
},
|
||||
pb_3xl: {
|
||||
paddingBottom: tokens.space._3xl,
|
||||
},
|
||||
pb_4xl: {
|
||||
paddingBottom: tokens.space._4xl,
|
||||
},
|
||||
pb_5xl: {
|
||||
paddingBottom: tokens.space._5xl,
|
||||
},
|
||||
pl_0: {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
pl_2xs: {
|
||||
paddingLeft: tokens.space._2xs,
|
||||
},
|
||||
pl_xs: {
|
||||
paddingLeft: tokens.space.xs,
|
||||
},
|
||||
pl_sm: {
|
||||
paddingLeft: tokens.space.sm,
|
||||
},
|
||||
pl_md: {
|
||||
paddingLeft: tokens.space.md,
|
||||
},
|
||||
pl_lg: {
|
||||
paddingLeft: tokens.space.lg,
|
||||
},
|
||||
pl_xl: {
|
||||
paddingLeft: tokens.space.xl,
|
||||
},
|
||||
pl_2xl: {
|
||||
paddingLeft: tokens.space._2xl,
|
||||
},
|
||||
pl_3xl: {
|
||||
paddingLeft: tokens.space._3xl,
|
||||
},
|
||||
pl_4xl: {
|
||||
paddingLeft: tokens.space._4xl,
|
||||
},
|
||||
pl_5xl: {
|
||||
paddingLeft: tokens.space._5xl,
|
||||
},
|
||||
pr_0: {
|
||||
paddingRight: 0,
|
||||
},
|
||||
pr_2xs: {
|
||||
paddingRight: tokens.space._2xs,
|
||||
},
|
||||
pr_xs: {
|
||||
paddingRight: tokens.space.xs,
|
||||
},
|
||||
pr_sm: {
|
||||
paddingRight: tokens.space.sm,
|
||||
},
|
||||
pr_md: {
|
||||
paddingRight: tokens.space.md,
|
||||
},
|
||||
pr_lg: {
|
||||
paddingRight: tokens.space.lg,
|
||||
},
|
||||
pr_xl: {
|
||||
paddingRight: tokens.space.xl,
|
||||
},
|
||||
pr_2xl: {
|
||||
paddingRight: tokens.space._2xl,
|
||||
},
|
||||
pr_3xl: {
|
||||
paddingRight: tokens.space._3xl,
|
||||
},
|
||||
pr_4xl: {
|
||||
paddingRight: tokens.space._4xl,
|
||||
},
|
||||
pr_5xl: {
|
||||
paddingRight: tokens.space._5xl,
|
||||
},
|
||||
|
||||
/*
|
||||
* Text decoration
|
||||
*/
|
||||
underline: {
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
strike_through: {
|
||||
textDecorationLine: 'line-through',
|
||||
},
|
||||
} as const
|
||||
@@ -0,0 +1,84 @@
|
||||
export const BLUE_HUE = 211
|
||||
export const RED_HUE = 346
|
||||
export const GREEN_HUE = 152
|
||||
|
||||
/**
|
||||
* Smooth progression of lightness "stops" for generating HSL colors.
|
||||
*/
|
||||
export const COLOR_STOPS = [
|
||||
0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1,
|
||||
]
|
||||
|
||||
export function generateScale(start: number, end: number) {
|
||||
const range = end - start
|
||||
return COLOR_STOPS.map(stop => {
|
||||
return start + range * stop
|
||||
})
|
||||
}
|
||||
|
||||
export const defaultScale = generateScale(6, 100)
|
||||
// dim shifted 6% lighter
|
||||
export const dimScale = generateScale(12, 100)
|
||||
|
||||
export const colors = {
|
||||
white: '#FFFFFF',
|
||||
black: '#000000',
|
||||
|
||||
contrast_0: `hsl(${BLUE_HUE}, 20%, ${defaultScale[14]}%)`,
|
||||
contrast_25: `hsl(${BLUE_HUE}, 20%, ${defaultScale[13]}%)`,
|
||||
contrast_50: `hsl(${BLUE_HUE}, 20%, ${defaultScale[12]}%)`,
|
||||
contrast_100: `hsl(${BLUE_HUE}, 20%, ${defaultScale[11]}%)`,
|
||||
contrast_200: `hsl(${BLUE_HUE}, 20%, ${defaultScale[10]}%)`,
|
||||
contrast_300: `hsl(${BLUE_HUE}, 20%, ${defaultScale[9]}%)`,
|
||||
contrast_400: `hsl(${BLUE_HUE}, 20%, ${defaultScale[8]}%)`,
|
||||
contrast_500: `hsl(${BLUE_HUE}, 20%, ${defaultScale[7]}%)`,
|
||||
contrast_600: `hsl(${BLUE_HUE}, 24%, ${defaultScale[6]}%)`,
|
||||
contrast_700: `hsl(${BLUE_HUE}, 24%, ${defaultScale[5]}%)`,
|
||||
contrast_800: `hsl(${BLUE_HUE}, 28%, ${defaultScale[4]}%)`,
|
||||
contrast_900: `hsl(${BLUE_HUE}, 28%, ${defaultScale[3]}%)`,
|
||||
contrast_950: `hsl(${BLUE_HUE}, 28%, ${defaultScale[2]}%)`,
|
||||
contrast_975: `hsl(${BLUE_HUE}, 28%, ${defaultScale[1]}%)`,
|
||||
contrast_1000: `hsl(${BLUE_HUE}, 28%, ${defaultScale[0]}%)`,
|
||||
|
||||
primary_25: `hsl(${BLUE_HUE}, 99%, 97%)`,
|
||||
primary_50: `hsl(${BLUE_HUE}, 99%, 95%)`,
|
||||
primary_100: `hsl(${BLUE_HUE}, 99%, 90%)`,
|
||||
primary_200: `hsl(${BLUE_HUE}, 99%, 80%)`,
|
||||
primary_300: `hsl(${BLUE_HUE}, 99%, 70%)`,
|
||||
primary_400: `hsl(${BLUE_HUE}, 99%, 60%)`,
|
||||
primary_500: `hsl(${BLUE_HUE}, 99%, 53%)`,
|
||||
primary_600: `hsl(${BLUE_HUE}, 99%, 42%)`,
|
||||
primary_700: `hsl(${BLUE_HUE}, 99%, 34%)`,
|
||||
primary_800: `hsl(${BLUE_HUE}, 99%, 26%)`,
|
||||
primary_900: `hsl(${BLUE_HUE}, 99%, 18%)`,
|
||||
primary_950: `hsl(${BLUE_HUE}, 99%, 10%)`,
|
||||
primary_975: `hsl(${BLUE_HUE}, 99%, 7%)`,
|
||||
|
||||
green_25: `hsl(${GREEN_HUE}, 82%, 97%)`,
|
||||
green_50: `hsl(${GREEN_HUE}, 82%, 95%)`,
|
||||
green_100: `hsl(${GREEN_HUE}, 82%, 90%)`,
|
||||
green_200: `hsl(${GREEN_HUE}, 82%, 80%)`,
|
||||
green_300: `hsl(${GREEN_HUE}, 82%, 70%)`,
|
||||
green_400: `hsl(${GREEN_HUE}, 82%, 60%)`,
|
||||
green_500: `hsl(${GREEN_HUE}, 82%, 50%)`,
|
||||
green_600: `hsl(${GREEN_HUE}, 82%, 42%)`,
|
||||
green_700: `hsl(${GREEN_HUE}, 82%, 34%)`,
|
||||
green_800: `hsl(${GREEN_HUE}, 82%, 26%)`,
|
||||
green_900: `hsl(${GREEN_HUE}, 82%, 18%)`,
|
||||
green_950: `hsl(${GREEN_HUE}, 82%, 10%)`,
|
||||
green_975: `hsl(${GREEN_HUE}, 82%, 7%)`,
|
||||
|
||||
red_25: `hsl(${RED_HUE}, 91%, 97%)`,
|
||||
red_50: `hsl(${RED_HUE}, 91%, 95%)`,
|
||||
red_100: `hsl(${RED_HUE}, 91%, 90%)`,
|
||||
red_200: `hsl(${RED_HUE}, 91%, 80%)`,
|
||||
red_300: `hsl(${RED_HUE}, 91%, 70%)`,
|
||||
red_400: `hsl(${RED_HUE}, 91%, 60%)`,
|
||||
red_500: `hsl(${RED_HUE}, 91%, 50%)`,
|
||||
red_600: `hsl(${RED_HUE}, 91%, 42%)`,
|
||||
red_700: `hsl(${RED_HUE}, 91%, 34%)`,
|
||||
red_800: `hsl(${RED_HUE}, 91%, 26%)`,
|
||||
red_900: `hsl(${RED_HUE}, 91%, 18%)`,
|
||||
red_950: `hsl(${RED_HUE}, 91%, 10%)`,
|
||||
red_975: `hsl(${RED_HUE}, 91%, 7%)`,
|
||||
} as const
|
||||
@@ -0,0 +1,100 @@
|
||||
import {colors} from './colors.js'
|
||||
import * as tokens from './tokens.js'
|
||||
|
||||
export {atoms} from './atoms.js'
|
||||
export * as tokens from './tokens.js'
|
||||
|
||||
const fontFamily = 'Inter'
|
||||
|
||||
export const theme = {
|
||||
palette: colors,
|
||||
atoms: {
|
||||
text: {
|
||||
color: colors.black,
|
||||
fontFamily,
|
||||
},
|
||||
text_contrast_low: {
|
||||
color: colors.contrast_500,
|
||||
fontFamily,
|
||||
},
|
||||
text_contrast_medium: {
|
||||
color: colors.contrast_700,
|
||||
fontFamily,
|
||||
},
|
||||
text_contrast_high: {
|
||||
color: colors.contrast_900,
|
||||
fontFamily,
|
||||
},
|
||||
text_inverted: {
|
||||
color: colors.white,
|
||||
fontFamily,
|
||||
},
|
||||
bg: {
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
bg_contrast_25: {
|
||||
backgroundColor: colors.contrast_25,
|
||||
},
|
||||
bg_contrast_50: {
|
||||
backgroundColor: colors.contrast_50,
|
||||
},
|
||||
bg_contrast_100: {
|
||||
backgroundColor: colors.contrast_100,
|
||||
},
|
||||
bg_contrast_200: {
|
||||
backgroundColor: colors.contrast_200,
|
||||
},
|
||||
bg_contrast_300: {
|
||||
backgroundColor: colors.contrast_300,
|
||||
},
|
||||
bg_contrast_400: {
|
||||
backgroundColor: colors.contrast_400,
|
||||
},
|
||||
bg_contrast_500: {
|
||||
backgroundColor: colors.contrast_500,
|
||||
},
|
||||
bg_contrast_600: {
|
||||
backgroundColor: colors.contrast_600,
|
||||
},
|
||||
bg_contrast_700: {
|
||||
backgroundColor: colors.contrast_700,
|
||||
},
|
||||
bg_contrast_800: {
|
||||
backgroundColor: colors.contrast_800,
|
||||
},
|
||||
bg_contrast_900: {
|
||||
backgroundColor: colors.contrast_900,
|
||||
},
|
||||
bg_contrast_950: {
|
||||
backgroundColor: colors.contrast_950,
|
||||
},
|
||||
bg_contrast_975: {
|
||||
backgroundColor: colors.contrast_975,
|
||||
},
|
||||
bg_primary_500: {
|
||||
backgroundColor: colors.primary_500,
|
||||
},
|
||||
border_contrast_low: {
|
||||
borderColor: colors.contrast_100,
|
||||
},
|
||||
border_contrast_medium: {
|
||||
borderColor: colors.contrast_200,
|
||||
},
|
||||
border_contrast_high: {
|
||||
borderColor: colors.contrast_300,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function style(styleObjects: Record<string, any>[]) {
|
||||
return Object.assign({}, ...styleObjects)
|
||||
}
|
||||
|
||||
export function gradient(color: keyof typeof tokens.gradients) {
|
||||
const {values} = tokens.gradients[color]
|
||||
return values
|
||||
.map(([pos, color]) => {
|
||||
return `${color} ${pos * 100}%`
|
||||
})
|
||||
.join(', ')
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
export const space = {
|
||||
_2xs: 2,
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
xl: 20,
|
||||
_2xl: 24,
|
||||
_3xl: 28,
|
||||
_4xl: 32,
|
||||
_5xl: 40,
|
||||
} as const
|
||||
|
||||
export const fontSize = {
|
||||
_2xs: 10,
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 18,
|
||||
xl: 20,
|
||||
_2xl: 22,
|
||||
_3xl: 26,
|
||||
_4xl: 32,
|
||||
_5xl: 40,
|
||||
} as const
|
||||
|
||||
export const lineHeight = {
|
||||
none: 1,
|
||||
normal: 1.5,
|
||||
relaxed: 1.625,
|
||||
} as const
|
||||
|
||||
export const borderRadius = {
|
||||
_2xs: 2,
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
full: 999,
|
||||
} as const
|
||||
|
||||
export const fontWeight = {
|
||||
normal: '400',
|
||||
semibold: '500',
|
||||
bold: '600',
|
||||
heavy: '700',
|
||||
} as const
|
||||
|
||||
export const gradients = {
|
||||
sky: {
|
||||
values: [
|
||||
[0, '#0A7AFF'],
|
||||
[1, '#59B9FF'],
|
||||
],
|
||||
hover_value: '#0A7AFF',
|
||||
},
|
||||
midnight: {
|
||||
values: [
|
||||
[0, '#022C5E'],
|
||||
[1, '#4079BC'],
|
||||
],
|
||||
hover_value: '#022C5E',
|
||||
},
|
||||
sunrise: {
|
||||
values: [
|
||||
[0, '#4E90AE'],
|
||||
[0.4, '#AEA3AB'],
|
||||
[0.8, '#E6A98F'],
|
||||
[1, '#F3A84C'],
|
||||
],
|
||||
hover_value: '#AEA3AB',
|
||||
},
|
||||
sunset: {
|
||||
values: [
|
||||
[0, '#6772AF'],
|
||||
[0.6, '#B88BB6'],
|
||||
[1, '#FFA6AC'],
|
||||
],
|
||||
hover_value: '#B88BB6',
|
||||
},
|
||||
summer: {
|
||||
values: [
|
||||
[0, '#FF6A56'],
|
||||
[0.3, '#FF9156'],
|
||||
[1, '#FFDD87'],
|
||||
],
|
||||
hover_value: '#FF9156',
|
||||
},
|
||||
nordic: {
|
||||
values: [
|
||||
[0, '#083367'],
|
||||
[1, '#9EE8C1'],
|
||||
],
|
||||
hover_value: '#3A7085',
|
||||
},
|
||||
bonfire: {
|
||||
values: [
|
||||
[0, '#203E4E'],
|
||||
[0.4, '#755B62'],
|
||||
[0.8, '#CD7765'],
|
||||
[1, '#EF956E'],
|
||||
],
|
||||
hover_value: '#755B62',
|
||||
},
|
||||
} as const
|
||||
@@ -0,0 +1,10 @@
|
||||
export function formatCount(count: number) {
|
||||
return Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
// `1,953` shouldn't be rounded up to 2k, it should be truncated.
|
||||
// @ts-ignore
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode
|
||||
roundingMode: 'trunc',
|
||||
}).format(count)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function formatDate(date: number | string | Date) {
|
||||
const d = new Date(date)
|
||||
return `${d.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})} at ${d.toLocaleTimeString(undefined, {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
})}`
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import MIMEType from 'whatwg-mimetype'
|
||||
|
||||
export async function getImage(url: string) {
|
||||
const response = await fetch(url)
|
||||
const mimeType = new MIMEType(response.headers.get('content-type') ?? '')
|
||||
const arrayBuf = await response.arrayBuffer() // must drain body even if it will be discarded
|
||||
if (response.status !== 200) return null
|
||||
return {
|
||||
mime: mimeType.essence as string | undefined,
|
||||
image: Buffer.from(arrayBuf),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {AppBskyGraphDefs, AtUri} from '@atproto/api'
|
||||
|
||||
const DEV = process.env.NODE_ENV === 'development'
|
||||
const HOST = DEV
|
||||
? `http://localhost:3000/start`
|
||||
: `https://ogcard.cdn.bsky.app/start`
|
||||
|
||||
export function getStarterPackImageUri(
|
||||
record: AppBskyGraphDefs.StarterPackViewBasic,
|
||||
) {
|
||||
const urip = new AtUri(record.uri)
|
||||
return `${HOST}/${record.creator.did}/${urip.rkey}`
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
import {
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyGraphDefs,
|
||||
AtpAgent,
|
||||
RichText,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {httpLogger} from '../logger.js'
|
||||
import {getImage} from './getImage.js'
|
||||
import {getStarterPackImageUri} from './getStarterPackImageUri.js'
|
||||
|
||||
export type Metadata = {
|
||||
aspectRatio: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
}
|
||||
|
||||
export type Image = Metadata & {
|
||||
mime: 'image/jpeg' | 'image/png' | string | undefined
|
||||
image: Buffer
|
||||
}
|
||||
|
||||
export type PostData = {
|
||||
images: Map<string, Image>
|
||||
texts: Map<string, RichText>
|
||||
}
|
||||
|
||||
function normalizeAspectRatio(aspectRatio?: {
|
||||
width: number
|
||||
height: number
|
||||
}): Metadata['aspectRatio'] {
|
||||
if (!aspectRatio)
|
||||
return {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
}
|
||||
return aspectRatio
|
||||
}
|
||||
|
||||
export async function resolvePostData(
|
||||
post: AppBskyFeedDefs.PostView,
|
||||
agent: AtpAgent,
|
||||
): Promise<PostData> {
|
||||
const images: Map<string, Metadata> = new Map()
|
||||
const texts: Map<string, RichText> = new Map()
|
||||
|
||||
// console.log(JSON.stringify(post, null, 2))
|
||||
|
||||
if (post.author.avatar) {
|
||||
images.set(post.author.avatar, {
|
||||
aspectRatio: {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (AppBskyFeedPost.isRecord(post.record) && post.record.text) {
|
||||
texts.set(post.record.text, new RichText({text: post.record.text}))
|
||||
}
|
||||
|
||||
if (post.embed) {
|
||||
if (AppBskyEmbedImages.isView(post.embed)) {
|
||||
// get OPs media
|
||||
for (const image of post.embed.images) {
|
||||
images.set(image.fullsize, {
|
||||
aspectRatio: normalizeAspectRatio(image.aspectRatio),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyEmbedExternal.isView(post.embed)) {
|
||||
if (post.embed.external.thumb) {
|
||||
images.set(post.embed.external.thumb, {
|
||||
aspectRatio: {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyEmbedRecord.isView(post.embed)) {
|
||||
if (AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
|
||||
if (post.embed.record.author.avatar) {
|
||||
images.set(post.embed.record.author.avatar, {
|
||||
aspectRatio: {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for (const embed of post.embed.record.embeds) {
|
||||
if (AppBskyEmbedImages.isView(embed)) {
|
||||
for (const image of embed.images) {
|
||||
images.set(image.fullsize, {
|
||||
aspectRatio: normalizeAspectRatio(image.aspectRatio),
|
||||
})
|
||||
}
|
||||
}
|
||||
if (AppBskyEmbedExternal.isView(embed)) {
|
||||
if (embed.external.thumb) {
|
||||
images.set(embed.external.thumb, {
|
||||
aspectRatio: {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||
if (AppBskyEmbedImages.isView(embed.media)) {
|
||||
for (const image of embed.media.images) {
|
||||
images.set(image.fullsize, {
|
||||
aspectRatio: normalizeAspectRatio(image.aspectRatio),
|
||||
})
|
||||
}
|
||||
} else if (AppBskyEmbedExternal.isView(embed.media)) {
|
||||
if (embed.media.external.thumb) {
|
||||
images.set(embed.media.external.thumb, {
|
||||
aspectRatio: {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyGraphDefs.isListView(embed.record)) {
|
||||
if (embed.record.avatar) {
|
||||
images.set(embed.record.avatar, {
|
||||
aspectRatio: {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
|
||||
if (embed.record.avatar) {
|
||||
images.set(embed.record.avatar, {
|
||||
aspectRatio: {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyGraphDefs.isStarterPackViewBasic(embed.record)) {
|
||||
const uri = getStarterPackImageUri(embed.record)
|
||||
images.set(uri, {
|
||||
aspectRatio: {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
AppBskyFeedPost.isRecord(post.embed.record.value) &&
|
||||
post.embed.record.value.text
|
||||
) {
|
||||
texts.set(
|
||||
post.embed.record.value.text,
|
||||
new RichText({text: post.embed.record.value.text}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyGraphDefs.isListView(post.embed.record)) {
|
||||
if (post.embed.record.avatar) {
|
||||
images.set(post.embed.record.avatar, {
|
||||
aspectRatio: {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyFeedDefs.isGeneratorView(post.embed.record)) {
|
||||
if (post.embed.record.avatar) {
|
||||
images.set(post.embed.record.avatar, {
|
||||
aspectRatio: {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyGraphDefs.isStarterPackViewBasic(post.embed.record)) {
|
||||
const uri = getStarterPackImageUri(post.embed.record)
|
||||
images.set(uri, {
|
||||
aspectRatio: {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
|
||||
// get OPs media
|
||||
if (AppBskyEmbedImages.isView(post.embed.media)) {
|
||||
for (const image of post.embed.media.images) {
|
||||
images.set(image.fullsize, {
|
||||
aspectRatio: normalizeAspectRatio(image.aspectRatio),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyEmbedExternal.isView(post.embed.media)) {
|
||||
if (post.embed.media.external.thumb) {
|
||||
images.set(post.embed.media.external.thumb, {
|
||||
aspectRatio: {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// get media from embedded post
|
||||
if (AppBskyEmbedRecord.isViewRecord(post.embed.record.record)) {
|
||||
for (const embed of post.embed.record.record.embeds) {
|
||||
if (AppBskyEmbedImages.isView(embed)) {
|
||||
for (const image of embed.images) {
|
||||
images.set(image.fullsize, {
|
||||
aspectRatio: normalizeAspectRatio(image.aspectRatio),
|
||||
})
|
||||
}
|
||||
}
|
||||
if (AppBskyEmbedExternal.isView(embed)) {
|
||||
if (embed.external.thumb) {
|
||||
images.set(embed.external.thumb, {
|
||||
aspectRatio: {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||
if (AppBskyEmbedImages.isView(embed.media)) {
|
||||
for (const image of embed.media.images) {
|
||||
images.set(image.fullsize, {
|
||||
aspectRatio: normalizeAspectRatio(image.aspectRatio),
|
||||
})
|
||||
}
|
||||
} else if (AppBskyEmbedExternal.isView(embed.media)) {
|
||||
if (embed.media.external.thumb) {
|
||||
images.set(embed.media.external.thumb, {
|
||||
aspectRatio: {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
AppBskyFeedPost.isRecord(post.embed.record.record.value) &&
|
||||
post.embed.record.record.value.text
|
||||
) {
|
||||
texts.set(
|
||||
post.embed.record.record.value.text,
|
||||
new RichText({text: post.embed.record.record.value.text}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deduped = Array.from(images.entries())
|
||||
const resolved = await Promise.all(
|
||||
deduped.map(async ([url, meta]) => {
|
||||
try {
|
||||
const image = await getImage(url)
|
||||
return [
|
||||
url,
|
||||
{
|
||||
...meta,
|
||||
...image,
|
||||
},
|
||||
] as const
|
||||
} catch (err) {
|
||||
httpLogger.warn({err, uri: url}, 'could not fetch image')
|
||||
return [
|
||||
url,
|
||||
{
|
||||
...meta,
|
||||
image: null,
|
||||
},
|
||||
] as const
|
||||
}
|
||||
}),
|
||||
)
|
||||
await Promise.all(Array.from(texts.values()).map(r => r.detectFacets(agent)))
|
||||
const extracted = resolved.filter(([, i]) => i.image !== null) as [
|
||||
string,
|
||||
Image,
|
||||
][]
|
||||
|
||||
return {
|
||||
images: new Map(extracted),
|
||||
texts,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export function toShortUrl(url: string): string {
|
||||
try {
|
||||
const urlp = new URL(url)
|
||||
if (urlp.protocol !== 'http:' && urlp.protocol !== 'https:') {
|
||||
return url
|
||||
}
|
||||
const path =
|
||||
(urlp.pathname === '/' ? '' : urlp.pathname) + urlp.search + urlp.hash
|
||||
if (path.length > 15) {
|
||||
return urlp.host + path.slice(0, 13) + '...'
|
||||
}
|
||||
return urlp.host + path
|
||||
} catch (e) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
+122
-19
@@ -2,15 +2,16 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@atproto/api@0.12.19-next.0":
|
||||
version "0.12.19-next.0"
|
||||
resolved "https://registry.npmjs.org/@atproto/api/-/api-0.12.19-next.0.tgz"
|
||||
integrity sha512-wyWr4uIabTgDTBY99y3QyrFxcIx1Mh4DkURgSv8sd/b+w0lfrZAJh0Gg9BXdg/iIjcf/M2lCTL04r0vASfkMVg==
|
||||
"@atproto/api@^0.13.4":
|
||||
version "0.13.4"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.4.tgz#47631e622760b2a18e4aee75b728e9cf8952e221"
|
||||
integrity sha512-Fwn37hP+Xr9YjA/hadvn7ZKbUPQhJiUus1+govgNKF3/jNdyk2ICoEe0z+hxaO3xX8LCU5yARbgt3SRoXbIwrg==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.3.0"
|
||||
"@atproto/lexicon" "^0.4.0"
|
||||
"@atproto/lexicon" "^0.4.1"
|
||||
"@atproto/syntax" "^0.3.0"
|
||||
"@atproto/xrpc" "^0.5.0"
|
||||
"@atproto/xrpc" "^0.6.1"
|
||||
await-lock "^2.2.2"
|
||||
multiformats "^9.9.0"
|
||||
tlds "^1.234.0"
|
||||
|
||||
@@ -36,29 +37,29 @@
|
||||
multiformats "^9.9.0"
|
||||
pino "^8.15.0"
|
||||
|
||||
"@atproto/lexicon@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.0.tgz"
|
||||
integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ==
|
||||
"@atproto/lexicon@^0.4.1":
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.1.tgz#19155210570a2fafbcc7d4f655d9b813948e72a0"
|
||||
integrity sha512-bzyr+/VHXLQWbumViX5L7h1NKQObfs8Z+XZJl43OUK8nYFUI4e/sW1IZKRNfw7Wvi5YVNK+J+yP3DWIBZhkCYA==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.3.0"
|
||||
"@atproto/syntax" "^0.3.0"
|
||||
iso-datestring-validator "^2.2.2"
|
||||
multiformats "^9.9.0"
|
||||
zod "^3.21.4"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/syntax@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.0.tgz"
|
||||
integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==
|
||||
|
||||
"@atproto/xrpc@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.5.0.tgz"
|
||||
integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==
|
||||
"@atproto/xrpc@^0.6.1":
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.1.tgz#dcd1315c8c60eef5af2db7fa4e35a38ebc6d79d5"
|
||||
integrity sha512-Zy5ydXEdk6sY7FDUZcEVfCL1jvbL4tXu5CcdPqbEaW6LQtk9GLds/DK1bCX9kswTGaBC88EMuqQMfkxOhp2t4A==
|
||||
dependencies:
|
||||
"@atproto/lexicon" "^0.4.0"
|
||||
zod "^3.21.4"
|
||||
"@atproto/lexicon" "^0.4.1"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@cbor-extract/cbor-extract-darwin-arm64@2.2.0":
|
||||
version "2.2.0"
|
||||
@@ -278,6 +279,11 @@ atomic-sleep@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz"
|
||||
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
|
||||
|
||||
await-lock@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef"
|
||||
integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
|
||||
|
||||
base64-js@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz"
|
||||
@@ -371,6 +377,11 @@ color-name@^1.1.4:
|
||||
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
colorette@^2.0.7:
|
||||
version "2.0.20"
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
||||
|
||||
content-disposition@0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz"
|
||||
@@ -422,6 +433,11 @@ css-to-react-native@^3.0.0:
|
||||
css-color-keywords "^1.0.0"
|
||||
postcss-value-parser "^4.0.2"
|
||||
|
||||
dateformat@^4.6.3:
|
||||
version "4.6.3"
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
|
||||
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
|
||||
|
||||
debug@2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
|
||||
@@ -478,6 +494,13 @@ encodeurl@~1.0.2:
|
||||
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz"
|
||||
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
es-define-property@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz"
|
||||
@@ -547,6 +570,11 @@ express@^4.19.2:
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
fast-copy@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35"
|
||||
integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==
|
||||
|
||||
fast-printf@^1.6.9:
|
||||
version "1.6.9"
|
||||
resolved "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.9.tgz"
|
||||
@@ -559,6 +587,11 @@ fast-redact@^3.1.1:
|
||||
resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz"
|
||||
integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==
|
||||
|
||||
fast-safe-stringify@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
||||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
||||
|
||||
fflate@^0.7.3:
|
||||
version "0.7.4"
|
||||
resolved "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz"
|
||||
@@ -653,6 +686,11 @@ hasown@^2.0.0:
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
help-me@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6"
|
||||
integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==
|
||||
|
||||
hex-rgb@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz"
|
||||
@@ -706,6 +744,11 @@ iso-datestring-validator@^2.2.2:
|
||||
resolved "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz"
|
||||
integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==
|
||||
|
||||
joycon@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
|
||||
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
||||
@@ -779,6 +822,11 @@ mime@1.6.0:
|
||||
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
|
||||
@@ -823,6 +871,13 @@ on-finished@2.4.1:
|
||||
dependencies:
|
||||
ee-first "1.1.1"
|
||||
|
||||
once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
p-finally@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz"
|
||||
@@ -865,7 +920,7 @@ path-to-regexp@0.1.7:
|
||||
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
|
||||
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
|
||||
|
||||
pino-abstract-transport@^1.2.0:
|
||||
pino-abstract-transport@^1.0.0, pino-abstract-transport@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz"
|
||||
integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==
|
||||
@@ -873,6 +928,26 @@ pino-abstract-transport@^1.2.0:
|
||||
readable-stream "^4.0.0"
|
||||
split2 "^4.0.0"
|
||||
|
||||
pino-pretty@^11.2.2:
|
||||
version "11.2.2"
|
||||
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.2.2.tgz#5e8ec69b31e90eb187715af07b1d29a544e60d39"
|
||||
integrity sha512-2FnyGir8nAJAqD3srROdrF1J5BIcMT4nwj7hHSc60El6Uxlym00UbCCd8pYIterstVBFlMyF1yFV8XdGIPbj4A==
|
||||
dependencies:
|
||||
colorette "^2.0.7"
|
||||
dateformat "^4.6.3"
|
||||
fast-copy "^3.0.2"
|
||||
fast-safe-stringify "^2.1.1"
|
||||
help-me "^5.0.0"
|
||||
joycon "^3.1.1"
|
||||
minimist "^1.2.6"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport "^1.0.0"
|
||||
pump "^3.0.0"
|
||||
readable-stream "^4.0.0"
|
||||
secure-json-parse "^2.4.0"
|
||||
sonic-boom "^4.0.1"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
pino-std-serializers@^6.0.0:
|
||||
version "6.2.2"
|
||||
resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz"
|
||||
@@ -940,6 +1015,14 @@ proxy-addr@~2.0.7:
|
||||
forwarded "0.2.0"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
qs@6.11.0:
|
||||
version "6.11.0"
|
||||
resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz"
|
||||
@@ -1030,6 +1113,11 @@ satori@^0.10.13:
|
||||
postcss-value-parser "^4.2.0"
|
||||
yoga-wasm-web "^0.3.3"
|
||||
|
||||
secure-json-parse@^2.4.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862"
|
||||
integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
|
||||
|
||||
semver-compare@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz"
|
||||
@@ -1127,6 +1215,11 @@ string_decoder@^1.3.0:
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
thread-stream@^2.6.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz"
|
||||
@@ -1253,6 +1346,16 @@ vary@~1.1.2:
|
||||
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz"
|
||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||
|
||||
whatwg-mimetype@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a"
|
||||
integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
@@ -1263,7 +1366,7 @@ yoga-wasm-web@^0.3.3:
|
||||
resolved "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz"
|
||||
integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==
|
||||
|
||||
zod@^3.21.4:
|
||||
zod@^3.21.4, zod@^3.23.8:
|
||||
version "3.23.8"
|
||||
resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz"
|
||||
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
||||
|
||||
Reference in New Issue
Block a user