Skip to content

types wip #186

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ jobs:

strategy:
matrix:
node-version: [14.x, 16.x]
node-version: [20.x, 22.x]

steps:
- uses: actions/checkout@v2
179 changes: 149 additions & 30 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -4,10 +4,12 @@
"description": "shared components for our websites",
"main": "dst/index.js",
"module": "dst/index.esm.js",
"types": "dst/types/index.d.ts",
"scripts": {
"build": "rimraf dst && microbundle src/index.js -o dst/index.js --jsx React.createElement -f modern,es,cjs --jsxFragment React.Fragment",
"watch": "microbundle watch src/index.js -o dst/index.js --jsx React.createElement -f modern,es,cjs --jsxFragment React.Fragment",
"format": "prettier --write 'src/**/*.js' '*.css'"
"clean": "rimraf dst",
"build": "npm run clean && tsc --emitDeclarationOnly && microbundle src/index.ts -o dst/index.js --jsx React.createElement -f modern,es,cjs --jsxFragment React.Fragment",
"watch": "npm run clean && microbundle watch src/index.ts -o dst/index.js --jsx React.createElement -f modern,es,cjs --jsxFragment React.Fragment & tsc --watch --emitDeclarationOnly",
"format": "prettier --write 'src/**/*.{ts,tsx,js}' '*.css'"
},
"repository": {
"type": "git",
@@ -45,8 +47,12 @@
},
"devDependencies": {
"@carbonplan/prettier": "^1.2.0",
"@types/node": "^22.13.10",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"microbundle": "^0.13.0",
"prettier": "^2.2.1",
"rimraf": "3.0.2"
"rimraf": "3.0.2",
"typescript": "^5.8.2"
}
}
65 changes: 45 additions & 20 deletions src/avatar-group.js → src/avatar-group.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react'
import { Box } from 'theme-ui'
import Avatar from './avatar'
import Row from './row'
import { Box, ResponsiveStyleValue } from 'theme-ui'
import Avatar, { AvatarProps } from './avatar'
import Row, { RowProps } from './row'
import Column from './column'
import Group from './group'
import Group, { GroupProps } from './group'

const sizes = {
xs: [1],
@@ -13,7 +13,14 @@ const sizes = {
xl: [9],
}

const Blank = ({ overflow, maxWidth }) => {
type SizeKey = keyof typeof sizes

type BlankProps = {
overflow: number
maxWidth?: string | number
}

const Blank = ({ overflow, maxWidth }: BlankProps) => {
return (
<Box
sx={{
@@ -45,6 +52,21 @@ const Blank = ({ overflow, maxWidth }) => {
)
}

type Alignment = 'left' | 'right'

type StartValue = 'auto' | number | (number | 'auto')[]

export interface AvatarGroupProps extends RowProps, GroupProps {
members: AvatarProps[]
direction?: 'horizontal' | 'vertical'
align?: Alignment | Alignment[]
spacing?: SizeKey | ResponsiveStyleValue<number | string>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh ResponsiveStyleValue is so handy!

limit?: number
width?: string
maxWidth?: string | number
fixedCount?: number
}

const AvatarGroup = ({
members,
direction = 'horizontal',
@@ -56,45 +78,48 @@ const AvatarGroup = ({
fixedCount,
sx,
...props
}) => {
let gap
if (sizes.hasOwnProperty(spacing)) {
gap = sizes[spacing]
}: AvatarGroupProps) => {
let gap: ResponsiveStyleValue<number | string>

if (typeof spacing === 'string' && spacing in sizes) {
gap = sizes[spacing as SizeKey]
} else {
gap = spacing
gap = spacing as ResponsiveStyleValue<number | string>
}

let start = (idx) => 'auto'
let start = (idx: number): StartValue => 'auto'
if (align) {
if (!Array.isArray(align)) {
align = [align]
}
start = (idx) =>
align.map((d) => {
start = (idx: number): StartValue => {
const alignArray = align as Alignment[]
return alignArray.map((d: Alignment) => {
if (d === 'left') {
return 'auto'
} else if (d === 'right') {
const offset = Math.max(1, fixedCount - members.length + 1)
return (offset + idx) % fixedCount
const offset = Math.max(1, (fixedCount ?? 0) - members.length + 1)
return (offset + idx) % (fixedCount ?? 1)
} else {
throw Error(`alignment '${align}' not recognized`)
throw Error(`alignment '${d}' not recognized`)
}
})
}
}

const excess = members.length > limit
const overflow = members.length - limit + 1
const excess = limit !== undefined && members.length > limit
const overflow = limit !== undefined ? members.length - limit + 1 : 0

return (
<>
{fixedCount && (
<Row columns={fixedCount} gap={gap} sx={sx} {...props}>
{members.map((props, idx) => (
<Column key={idx} start={start(idx)}>
{(!excess || idx < limit - 1) && (
{(!excess || (limit !== undefined && idx < limit - 1)) && (
<Avatar {...props} width={width} maxWidth={maxWidth} />
)}
{excess && idx === limit - 1 && (
{excess && limit !== undefined && idx === limit - 1 && (
<Blank overflow={overflow} maxWidth={maxWidth} />
)}
</Column>
14 changes: 12 additions & 2 deletions src/avatar.js → src/avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import React from 'react'
import { Box, Image } from 'theme-ui'
import { Box, Image, BoxProps } from 'theme-ui'

export interface AvatarProps extends BoxProps {
color?: string
width?: string
maxWidth?: string | number
name?: string
github?: string
alt?: string
src?: string
}

const Avatar = ({
color = 'transparent',
@@ -11,7 +21,7 @@ const Avatar = ({
src,
sx,
...props
}) => {
}: AvatarProps) => {
if (!name && !src && !github) {
console.warn('must specify either name, github, or src')
}
10 changes: 8 additions & 2 deletions src/badge.js → src/badge.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import React from 'react'
import { Box } from 'theme-ui'
import { Box, BoxProps, ThemeUIStyleObject } from 'theme-ui'
import { transparentize } from '@theme-ui/color'

const Badge = ({ sx, children, ...props }) => {
export interface BadgeProps extends BoxProps {
sx?: ThemeUIStyleObject & {
color?: string // ThemeUIStyleObject doesn't have a color property
}
}

const Badge = ({ sx, children, ...props }: BadgeProps) => {
const color = sx && sx.color ? sx.color : 'primary'
return (
<Box
9 changes: 5 additions & 4 deletions src/blockquote.js → src/blockquote.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import React, { Children } from 'react'
import React, { Children, PropsWithChildren } from 'react'
import { Box } from 'theme-ui'

export type BlockquoteProps = PropsWithChildren<{}>

const specialChars = ['“', '"', "'", '‘']

const Blockquote = ({ children }) => {
const Blockquote = ({ children }: BlockquoteProps) => {
return (
<Box variant='styles.blockquote'>
{Children.map(children, (d, i) => {
let firstChar = ''
let remaining = children

if (d.props && typeof d.props.children === 'string') {
if (React.isValidElement(d) && typeof d.props.children === 'string') {
firstChar = d.props.children.slice(0, 1)
remaining = d.props.children.slice(1)
} else if (typeof d === 'string') {
71 changes: 52 additions & 19 deletions src/button.js → src/button.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
import React, { forwardRef, cloneElement } from 'react'
import { Box } from 'theme-ui'
import Link from './link'
import { Box, BoxProps, ThemeUIStyleObject } from 'theme-ui'
import Link, { LinkProps } from './link'
import getSizeStyles from './utils/get-size-styles'

const hasCustomHover = (comp: any): comp is { hover: ThemeUIStyleObject } =>
!!comp?.hover

export interface ButtonProps extends Omit<BoxProps, 'prefix'> {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
align?:
| 'baseline'
| 'sub'
| 'super'
| 'text-top'
| 'text-bottom'
| 'middle'
| 'top'
| 'bottom'
| 'initial'
suffix?: React.ReactElement & { props?: { sx?: ThemeUIStyleObject } }
prefix?: React.ReactElement & { props?: { sx?: ThemeUIStyleObject } }
inverted?: boolean
href?: string
internal?: boolean
sx?: ThemeUIStyleObject & {
color?: string // ThemeUIStyleObject doesn't have a color property
}
}

const Button = (
{
size = 'sm',
@@ -15,14 +40,14 @@ const Button = (
href,
internal,
...props
},
ref
}: ButtonProps,
ref: React.Ref<HTMLButtonElement | HTMLAnchorElement>
) => {
if (!['xs', 'sm', 'md', 'lg', 'xl'].includes(size)) {
throw new Error('Size must be xs, sm, md, lg, or xl')
}

let offset, margin, top, height, width, strokeWidth
let offset, margin, height, width, strokeWidth

const { color, ...sxProp } = sx || {}

@@ -99,14 +124,16 @@ const Button = (
suffixOffset = offset
}

let clonedPrefix, clonedSuffix

if (prefix) {
prefixHover = {
'&:hover > #prefix-span > #prefix': {
color: hoverColor,
...prefix.type.hover,
...(hasCustomHover(prefix.type) ? prefix.type.hover : {}),
},
}
prefix = cloneElement(prefix, {
clonedPrefix = cloneElement(prefix, {
id: 'prefix',
sx: {
position: 'relative',
@@ -116,7 +143,7 @@ const Button = (
strokeWidth: strokeWidth,
verticalAlign: prefixAlign,
transition: 'color 0.15s, transform 0.15s',
...prefix.props.sx,
...prefix.props?.sx,
},
})
}
@@ -125,10 +152,10 @@ const Button = (
suffixHover = {
'&:hover > #suffix-span >#suffix': {
color: hoverColor,
...suffix.type.hover,
...(hasCustomHover(suffix.type) ? suffix.type.hover : {}),
},
}
suffix = cloneElement(suffix, {
clonedSuffix = cloneElement(suffix, {
id: 'suffix',
sx: {
height: height,
@@ -137,7 +164,7 @@ const Button = (
strokeWidth: strokeWidth,
verticalAlign: suffixAlign,
transition: 'color 0.15s, transform 0.15s',
...suffix.props.sx,
...suffix.props?.sx,
},
})
}
@@ -152,7 +179,7 @@ const Button = (
display: 'block',
color: baseColor,
padding: [0],
textAlign: 'left',
textAlign: 'left' as const,
cursor: 'pointer',
width: 'fit-content',
'@media (hover: hover) and (pointer: fine)': {
@@ -172,7 +199,7 @@ const Button = (
id='prefix-span'
sx={{ display: 'inline-block', ...prefixOffset }}
>
{prefix && prefix}
{clonedPrefix}
</Box>
<Box as='span' sx={{ transition: 'color 0.15s' }}>
{children}
@@ -182,33 +209,39 @@ const Button = (
id='suffix-span'
sx={{ display: 'inline-block', ...suffixOffset }}
>
{suffix && suffix}
{clonedSuffix}
</Box>
</>
)

if (href) {
return (
<Link
ref={ref}
href={href}
ref={ref as React.Ref<HTMLAnchorElement>}
internal={internal}
sx={{
...style,
textDecoration: 'none',
}}
{...props}
{...(props as LinkProps)}
>
{Inner}
</Link>
)
} else {
return (
<Box ref={ref} as='button' sx={style} {...props}>
<Box
ref={ref as React.Ref<HTMLButtonElement>}
as='button'
sx={style}
{...props}
>
{Inner}
</Box>
)
}
}

export default forwardRef(Button)
export default forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonProps>(
Button
)
Loading
Loading