Handle images

This commit is contained in:
Eric Bailey
2025-03-16 13:44:49 -05:00
parent 5a4e75238f
commit 223ba68a0b
11 changed files with 211 additions and 28 deletions
+1
View File
@@ -25,6 +25,7 @@
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.3", "@types/node": "^20.14.3",
"pino-pretty": "^11.2.2", "pino-pretty": "^11.2.2",
"prettier": "^3.5.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }
+1 -1
View File
@@ -7,7 +7,7 @@ export function Box({
cx = [], cx = [],
}: { }: {
children?: React.ReactNode children?: React.ReactNode
cx?: Record<string, any>[] cx?: (Record<string, any> | undefined)[]
}) { }) {
return <div style={s([a.flex, a.flex_col, ...cx])}>{children}</div> return <div style={s([a.flex, a.flex_col, ...cx])}>{children}</div>
} }
+1 -1
View File
@@ -29,7 +29,7 @@ export function Row({
export function Column({ export function Column({
children, children,
gutter = 0, gutter,
cx, cx,
width = 1, width = 1,
}: { }: {
+13 -3
View File
@@ -1,8 +1,9 @@
import React from 'react' import React from 'react'
import {Image as ImageSource} from '../data/getPostData.js' import {Image as ImageSource} from '../data/getPostData.js'
import {atoms as a, style as s, theme as t} from '../theme/index.js' import {atoms as a, style as s, StyleProp,theme as t} from '../theme/index.js'
import {Box} from './Box.js' import {Box} from './Box.js'
import {MediaInsetBorder} from './MediaInsetBorder.js'
export type ImageProps = Omit< export type ImageProps = Omit<
React.ImgHTMLAttributes<HTMLImageElement>, React.ImgHTMLAttributes<HTMLImageElement>,
@@ -23,16 +24,24 @@ export function Image({image, cx, ...rest}: ImageProps) {
) )
} }
export function SquareImage({image}: {image: ImageSource}) { export function SquareImage({
image,
style,
insetBorderStyle,
}: {
image: ImageSource
insetBorderStyle?: StyleProp['style']
} & StyleProp) {
return ( return (
<Box <Box
cx={[ cx={[
a.relative, a.relative,
a.rounded_sm, a.rounded_md,
a.overflow_hidden, a.overflow_hidden,
a.w_full, a.w_full,
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
{paddingTop: '100%'}, {paddingTop: '100%'},
style,
]}> ]}>
<Image <Image
image={image} image={image}
@@ -44,6 +53,7 @@ export function SquareImage({image}: {image: ImageSource}) {
}, },
]} ]}
/> />
<MediaInsetBorder style={insetBorderStyle} />
</Box> </Box>
) )
} }
@@ -0,0 +1,46 @@
import React from 'react'
import {atoms as a, StyleProp,theme as t} from '../theme/index.js'
import {Box} from './Box.js'
/**
* Applies and thin border within a bounding box. Used to contrast media from
* bg of the container.
*/
export function MediaInsetBorder({
children,
style,
opaque,
}: {
children?: React.ReactNode
/**
* Used where this border needs to match adjacent borders, such as in
* external link previews
*/
opaque?: boolean
} & StyleProp) {
const isLight = t.name === 'light'
return (
<Box
cx={[
a.absolute,
a.inset_0,
a.rounded_md,
a.border,
opaque
? [t.atoms.border_contrast_low]
: [
isLight
? t.atoms.border_contrast_low
: t.atoms.border_contrast_high,
{opacity: 0.6},
],
{
pointerEvents: 'none',
},
style,
]}>
{children}
</Box>
)
}
+2 -1
View File
@@ -10,6 +10,7 @@ import {atoms as a, theme as t} from '../../theme/index.js'
import {formatCount} from '../../util/formatCount.js' import {formatCount} from '../../util/formatCount.js'
import {formatDate} from '../../util/formatDate.js' import {formatDate} from '../../util/formatDate.js'
import {moderatePost} from '../../util/moderatePost.js' import {moderatePost} from '../../util/moderatePost.js'
import {sanitizeHandle} from '../../util/sanitizeHandle.js'
import {Avatar} from '../Avatar.js' import {Avatar} from '../Avatar.js'
import {Box} from '../Box.js' import {Box} from '../Box.js'
import {Heart} from '../icons/Heart.js' import {Heart} from '../icons/Heart.js'
@@ -85,7 +86,7 @@ export function Post({
{post.author.displayName || post.author.handle} {post.author.displayName || post.author.handle}
</Text> </Text>
<Text cx={[a.text_sm, t.atoms.text_contrast_medium]}> <Text cx={[a.text_sm, t.atoms.text_contrast_medium]}>
@{post.author.handle} {sanitizeHandle(post.author.handle, '@')}
</Text> </Text>
</Box> </Box>
</Box> </Box>
+123 -20
View File
@@ -8,14 +8,14 @@ import {ModeratorData} from '../data/getModeratorData.js'
import {Image as ImageSource, PostData} from '../data/getPostData.js' import {Image as ImageSource, PostData} from '../data/getPostData.js'
import {atoms as a} from '../theme/index.js' import {atoms as a} from '../theme/index.js'
import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js' import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js'
import {Embed, EmbedType,parseEmbed} from '../util/parseEmbed.js' import {Embed, EmbedType, parseEmbed} from '../util/parseEmbed.js'
import {Box} from './Box.js' import {Box} from './Box.js'
import {FeedCard} from './FeedCard.js' import {FeedCard} from './FeedCard.js'
import * as Grid from './Grid.js' import * as Grid from './Grid.js'
import {Image, SquareImage} from './Image.js' import {Image, SquareImage} from './Image.js'
import {LinkCard} from './LinkCard.js' import {LinkCard} from './LinkCard.js'
import {ListCard} from './ListCard.js' import {ListCard} from './ListCard.js'
import {NotQuotePost,QuotePost} from './PostEmbed/QuotePost.js' import {NotQuotePost, QuotePost} from './PostEmbed/QuotePost.js'
type CommonProps = { type CommonProps = {
data: PostData data: PostData
@@ -188,6 +188,7 @@ export function ImagesEmbed({
embed: EmbedType<'images'> embed: EmbedType<'images'>
}) { }) {
const {images} = embed.view const {images} = embed.view
const gutter = a.p_2xs.padding
if (images.length > 0) { if (images.length > 0) {
const imgs = images const imgs = images
@@ -201,37 +202,139 @@ export function ImagesEmbed({
) )
} else if (imgs.length === 2) { } else if (imgs.length === 2) {
return ( return (
<Grid.Row gutter={a.p_xs.padding}> <Grid.Row gutter={gutter}>
<Grid.Column gutter={a.p_xs.padding} width={1 / 2}> <Grid.Column width={1 / 2} gutter={gutter}>
<SquareImage image={imgs[0]} /> <SquareImage
image={imgs[0]}
style={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
}}
insetBorderStyle={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
}}
/>
</Grid.Column> </Grid.Column>
<Grid.Column gutter={a.p_xs.padding} width={1 / 2}> <Grid.Column width={1 / 2} gutter={gutter}>
<SquareImage image={imgs[1]} /> <SquareImage
image={imgs[1]}
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
insetBorderStyle={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
/>
</Grid.Column> </Grid.Column>
</Grid.Row> </Grid.Row>
) )
} else if (imgs.length === 3) { } else if (imgs.length === 3) {
return ( return (
<Grid.Row gutter={a.p_xs.padding}> <Grid.Row gutter={gutter}>
<Grid.Column gutter={a.p_xs.padding} width={2 / 3}> <Grid.Column gutter={gutter} width={2 / 3}>
<SquareImage image={imgs[0]} /> <SquareImage
image={imgs[0]}
style={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
}}
insetBorderStyle={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
}}
/>
</Grid.Column> </Grid.Column>
<Grid.Column gutter={a.p_xs.padding} width={1 / 3} cx={[a.gap_sm]}> <Grid.Column gutter={gutter} width={1 / 3} cx={[a.gap_xs]}>
<SquareImage image={imgs[1]} /> <SquareImage
<SquareImage image={imgs[2]} /> image={imgs[1]}
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
}}
insetBorderStyle={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
}}
/>
<SquareImage
image={imgs[2]}
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopRightRadius: 0,
}}
insetBorderStyle={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopRightRadius: 0,
}}
/>
</Grid.Column> </Grid.Column>
</Grid.Row> </Grid.Row>
) )
} else { } else {
return ( return (
<Grid.Row gutter={a.p_xs.padding}> <Grid.Row gutter={gutter}>
<Grid.Column gutter={a.p_xs.padding} width={1 / 2} cx={[a.gap_sm]}> <Grid.Column gutter={gutter} width={1 / 2} cx={[a.gap_xs]}>
<SquareImage image={imgs[0]} /> <SquareImage
<SquareImage image={imgs[1]} /> image={imgs[0]}
style={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderBottomLeftRadius: 0,
}}
insetBorderStyle={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderBottomLeftRadius: 0,
}}
/>
<SquareImage
image={imgs[2]}
style={{
borderTopLeftRadius: 0,
borderBottomRightRadius: 0,
borderTopRightRadius: 0,
}}
insetBorderStyle={{
borderTopLeftRadius: 0,
borderBottomRightRadius: 0,
borderTopRightRadius: 0,
}}
/>
</Grid.Column> </Grid.Column>
<Grid.Column gutter={a.p_xs.padding} width={1 / 2} cx={[a.gap_sm]}> <Grid.Column gutter={gutter} width={1 / 2} cx={[a.gap_xs]}>
<SquareImage image={imgs[2]} /> <SquareImage
<SquareImage image={imgs[3]} /> image={imgs[1]}
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
}}
insetBorderStyle={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
}}
/>
<SquareImage
image={imgs[3]}
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopRightRadius: 0,
}}
insetBorderStyle={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopRightRadius: 0,
}}
/>
</Grid.Column> </Grid.Column>
</Grid.Row> </Grid.Row>
) )
@@ -10,6 +10,7 @@ import {PostData} from '../../data/getPostData.js'
import {atoms as a, theme as t} from '../../theme/index.js' import {atoms as a, theme as t} from '../../theme/index.js'
import {getModerationCauseInfo} from '../../util/getModerationCauseInfo.js' import {getModerationCauseInfo} from '../../util/getModerationCauseInfo.js'
import {moderatePost} from '../../util/moderatePost.js' import {moderatePost} from '../../util/moderatePost.js'
import {sanitizeHandle} from '../../util/sanitizeHandle.js'
import {viewRecordToPostView} from '../../util/viewRecordToPostView.js' import {viewRecordToPostView} from '../../util/viewRecordToPostView.js'
import {Avatar} from '../Avatar.js' import {Avatar} from '../Avatar.js'
import {Box} from '../Box.js' import {Box} from '../Box.js'
@@ -71,7 +72,7 @@ export function QuotePost({
{author.displayName || author.handle} {author.displayName || author.handle}
</Text> </Text>
<Text cx={[a.text_xs, t.atoms.text_contrast_medium]}> <Text cx={[a.text_xs, t.atoms.text_contrast_medium]}>
@{author.handle} {sanitizeHandle(author.handle, '@')}
</Text> </Text>
</Box> </Box>
</Box> </Box>
+10 -1
View File
@@ -7,6 +7,7 @@ export * as tokens from './tokens.js'
const fontFamily = 'Inter' const fontFamily = 'Inter'
export const theme = { export const theme = {
name: 'light',
palette: colors, palette: colors,
atoms: { atoms: {
text: { text: {
@@ -86,7 +87,15 @@ export const theme = {
}, },
} }
export function style(styleObjects: Record<string, any>[]) { export type StyleObject = Record<string, any>
export type StyleProp = {
style?: StyleObject
}
export function style(
styleObjects: (StyleObject | boolean | null | undefined)[],
) {
return Object.assign({}, ...styleObjects.filter(Boolean).flat()) return Object.assign({}, ...styleObjects.filter(Boolean).flat())
} }
+7
View File
@@ -0,0 +1,7 @@
export function isInvalidHandle(handle: string): boolean {
return handle === 'handle.invalid'
}
export function sanitizeHandle(handle: string, prefix = ''): string {
return isInvalidHandle(handle) ? '⚠Invalid Handle' : `${prefix}${handle}`
}
+5
View File
@@ -1309,6 +1309,11 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0:
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
prettier@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5"
integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==
process-warning@^3.0.0: process-warning@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz" resolved "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz"