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": {
"@types/node": "^20.14.3",
"pino-pretty": "^11.2.2",
"prettier": "^3.5.3",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
}
+1 -1
View File
@@ -7,7 +7,7 @@ export function Box({
cx = [],
}: {
children?: React.ReactNode
cx?: Record<string, any>[]
cx?: (Record<string, any> | undefined)[]
}) {
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({
children,
gutter = 0,
gutter,
cx,
width = 1,
}: {
+13 -3
View File
@@ -1,8 +1,9 @@
import React from 'react'
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 {MediaInsetBorder} from './MediaInsetBorder.js'
export type ImageProps = Omit<
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 (
<Box
cx={[
a.relative,
a.rounded_sm,
a.rounded_md,
a.overflow_hidden,
a.w_full,
t.atoms.bg_contrast_25,
{paddingTop: '100%'},
style,
]}>
<Image
image={image}
@@ -44,6 +53,7 @@ export function SquareImage({image}: {image: ImageSource}) {
},
]}
/>
<MediaInsetBorder style={insetBorderStyle} />
</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 {formatDate} from '../../util/formatDate.js'
import {moderatePost} from '../../util/moderatePost.js'
import {sanitizeHandle} from '../../util/sanitizeHandle.js'
import {Avatar} from '../Avatar.js'
import {Box} from '../Box.js'
import {Heart} from '../icons/Heart.js'
@@ -85,7 +86,7 @@ export function Post({
{post.author.displayName || post.author.handle}
</Text>
<Text cx={[a.text_sm, t.atoms.text_contrast_medium]}>
@{post.author.handle}
{sanitizeHandle(post.author.handle, '@')}
</Text>
</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 {atoms as a} from '../theme/index.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 {FeedCard} from './FeedCard.js'
import * as Grid from './Grid.js'
import {Image, SquareImage} from './Image.js'
import {LinkCard} from './LinkCard.js'
import {ListCard} from './ListCard.js'
import {NotQuotePost,QuotePost} from './PostEmbed/QuotePost.js'
import {NotQuotePost, QuotePost} from './PostEmbed/QuotePost.js'
type CommonProps = {
data: PostData
@@ -188,6 +188,7 @@ export function ImagesEmbed({
embed: EmbedType<'images'>
}) {
const {images} = embed.view
const gutter = a.p_2xs.padding
if (images.length > 0) {
const imgs = images
@@ -201,37 +202,139 @@ export function ImagesEmbed({
)
} 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.Row gutter={gutter}>
<Grid.Column width={1 / 2} gutter={gutter}>
<SquareImage
image={imgs[0]}
style={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
}}
insetBorderStyle={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
}}
/>
</Grid.Column>
<Grid.Column gutter={a.p_xs.padding} width={1 / 2}>
<SquareImage image={imgs[1]} />
<Grid.Column width={1 / 2} gutter={gutter}>
<SquareImage
image={imgs[1]}
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
insetBorderStyle={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
/>
</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.Row gutter={gutter}>
<Grid.Column gutter={gutter} width={2 / 3}>
<SquareImage
image={imgs[0]}
style={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
}}
insetBorderStyle={{
borderTopRightRadius: 0,
borderBottomRightRadius: 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 gutter={gutter} width={1 / 3} cx={[a.gap_xs]}>
<SquareImage
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.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.Row gutter={gutter}>
<Grid.Column gutter={gutter} width={1 / 2} cx={[a.gap_xs]}>
<SquareImage
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 gutter={a.p_xs.padding} width={1 / 2} cx={[a.gap_sm]}>
<SquareImage image={imgs[2]} />
<SquareImage image={imgs[3]} />
<Grid.Column gutter={gutter} width={1 / 2} cx={[a.gap_xs]}>
<SquareImage
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.Row>
)
@@ -10,6 +10,7 @@ import {PostData} from '../../data/getPostData.js'
import {atoms as a, theme as t} from '../../theme/index.js'
import {getModerationCauseInfo} from '../../util/getModerationCauseInfo.js'
import {moderatePost} from '../../util/moderatePost.js'
import {sanitizeHandle} from '../../util/sanitizeHandle.js'
import {viewRecordToPostView} from '../../util/viewRecordToPostView.js'
import {Avatar} from '../Avatar.js'
import {Box} from '../Box.js'
@@ -71,7 +72,7 @@ export function QuotePost({
{author.displayName || author.handle}
</Text>
<Text cx={[a.text_xs, t.atoms.text_contrast_medium]}>
@{author.handle}
{sanitizeHandle(author.handle, '@')}
</Text>
</Box>
</Box>
+10 -1
View File
@@ -7,6 +7,7 @@ export * as tokens from './tokens.js'
const fontFamily = 'Inter'
export const theme = {
name: 'light',
palette: colors,
atoms: {
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())
}
+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"
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:
version "3.0.0"
resolved "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz"