Handle images
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function Row({
|
|||||||
|
|
||||||
export function Column({
|
export function Column({
|
||||||
children,
|
children,
|
||||||
gutter = 0,
|
gutter,
|
||||||
cx,
|
cx,
|
||||||
width = 1,
|
width = 1,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user