@spies-ui/react 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@spies-ui/react",
3
+ "version": "1.0.1",
4
+ "description": "",
5
+ "main": "./dist/index.mjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsup src/index.tsx --format esm,cjs --dts --external react",
10
+ "dev": "tsup src/index.tsx --format esm,cjs --dts --external react --watch",
11
+ "lint": "eslint src/**/*.ts* --fix"
12
+ },
13
+ "keywords": [],
14
+ "author": "",
15
+ "license": "MIT",
16
+ "devDependencies": {
17
+ "@spies-ui/eslint-config": "*",
18
+ "@spies-ui/tokens": "*",
19
+ "@spies-ui/ts-config": "*",
20
+ "@types/react": "^18.2.79",
21
+ "@types/react-dom": "^18.2.25",
22
+ "react": "^18.2.0",
23
+ "tsup": "^8.0.2",
24
+ "typescript": "^5.4.5"
25
+ },
26
+ "dependencies": {
27
+ "@radix-ui/react-avatar": "^1.0.4",
28
+ "@radix-ui/react-checkbox": "^1.0.4",
29
+ "@stitches/react": "^1.2.8",
30
+ "phosphor-react": "^1.4.1"
31
+ }
32
+ }
@@ -0,0 +1,19 @@
1
+ import { ComponentProps } from 'react'
2
+ import { AvatarContainer, AvatarFallback, AvatarImage } from './styles'
3
+ import { User } from 'phosphor-react'
4
+
5
+ export interface AvatarProps extends ComponentProps<typeof AvatarImage> {}
6
+
7
+ export function Avatar(props: AvatarProps) {
8
+ return (
9
+ <AvatarContainer>
10
+ <AvatarImage {...props} />
11
+
12
+ <AvatarFallback delayMs={600}>
13
+ <User />
14
+ </AvatarFallback>
15
+ </AvatarContainer>
16
+ )
17
+ }
18
+
19
+ Avatar.displayName = 'Avatar'
@@ -0,0 +1,31 @@
1
+ import * as Avatar from '@radix-ui/react-avatar'
2
+ import { styled } from '../../styles'
3
+
4
+ export const AvatarContainer = styled(Avatar.Root, {
5
+ borderRadius: '$full',
6
+ display: 'inline-block',
7
+ width: '$12',
8
+ height: '$12',
9
+ overflow: 'hidden',
10
+ })
11
+
12
+ export const AvatarImage = styled(Avatar.Image, {
13
+ width: '100%',
14
+ height: '100%',
15
+ objectFix: 'cover',
16
+ borderRadius: 'inherit',
17
+ })
18
+ export const AvatarFallback = styled(Avatar.Fallback, {
19
+ width: '100%',
20
+ height: '100%',
21
+ display: 'flex',
22
+ alignItems: 'center',
23
+ justifyContent: 'center',
24
+ backgroundColor: '$gray600',
25
+ color: '$gray800',
26
+
27
+ svg: {
28
+ width: '$6',
29
+ height: '$6',
30
+ },
31
+ })
@@ -0,0 +1,32 @@
1
+ import { ComponentProps, ElementType } from 'react'
2
+ import { styled } from '../styles'
3
+
4
+ export const Box = styled('div', {
5
+ padding: '$4',
6
+ borderRadius: '$md',
7
+ variants: {
8
+ background: {
9
+ default: {
10
+ backgroundColor: 'transparent',
11
+ },
12
+ gray600: {
13
+ backgroundColor: '$gray600',
14
+ },
15
+ gray700: {
16
+ backgroundColor: '$gray700',
17
+ },
18
+ gray800: {
19
+ backgroundColor: '$gray800',
20
+ },
21
+ gray900: {
22
+ backgroundColor: '$gray900',
23
+ },
24
+ },
25
+ },
26
+ })
27
+
28
+ export interface BoxProps extends ComponentProps<typeof Box> {
29
+ as?: ElementType
30
+ }
31
+
32
+ Box.displayName = 'Box'
@@ -0,0 +1,97 @@
1
+ import { ComponentProps, ElementType } from 'react'
2
+ import { styled } from '../styles'
3
+
4
+ export const Button = styled('button', {
5
+ all: 'unset',
6
+ fontFamily: '$default',
7
+ fontWeight: '$medium',
8
+ fontSize: '$sm',
9
+ textAlign: 'center',
10
+ minWidth: 0,
11
+ boxSizing: 'border-box',
12
+
13
+ display: 'flex',
14
+ alignItems: 'center',
15
+ justifyContent: 'center',
16
+ gap: '$2',
17
+
18
+ cursor: 'pointer',
19
+
20
+ transition: '0.2s',
21
+
22
+ svg: {
23
+ width: '$4',
24
+ height: '$4',
25
+ },
26
+
27
+ '&:disabled': {
28
+ cursor: 'not-allowed',
29
+ backgroundColor: '$gray200',
30
+ borderColor: '$gray200',
31
+ },
32
+
33
+ variants: {
34
+ variant: {
35
+ default: {
36
+ backgroundColor: 'transparent',
37
+ border: '2px solid $gray500',
38
+ color: '$smokedWhite',
39
+
40
+ '&:not(:disabled):hover': {
41
+ // backgroundColor: '$smokedWhite',
42
+ // borderColor: '$smokedWhite',
43
+ // color: '$gray900',
44
+ borderColor: '$gray600',
45
+ },
46
+ },
47
+ primary: {
48
+ backgroundColor: '$primary',
49
+ border: '2px solid $primary',
50
+ color: '$smokedWhite',
51
+
52
+ '&:not(:disabled):hover': {
53
+ backgroundColor: '$primaryLight',
54
+ borderColor: '$primaryLight',
55
+ },
56
+ },
57
+ },
58
+
59
+ size: {
60
+ default: {
61
+ padding: '$3 $6',
62
+ },
63
+ fit: {
64
+ width: '100%',
65
+ paddingTop: '$3',
66
+ paddingBottom: '$3',
67
+ },
68
+ small: {
69
+ padding: '$3 $4',
70
+ },
71
+ },
72
+
73
+ radius: {
74
+ small: {
75
+ borderRadius: '$sm',
76
+ },
77
+ default: {
78
+ borderRadius: '$lg',
79
+ },
80
+ full: {
81
+ borderRadius: '$full',
82
+ },
83
+ },
84
+ },
85
+
86
+ defaultVariants: {
87
+ size: 'default',
88
+ radius: 'default',
89
+ variant: 'default',
90
+ },
91
+ })
92
+
93
+ export interface ButtonProps extends ComponentProps<typeof Button> {
94
+ as?: ElementType
95
+ }
96
+
97
+ Button.displayName = 'Button'
@@ -0,0 +1,28 @@
1
+ import { Check } from 'phosphor-react'
2
+ import { CheckboxContainer, CheckboxIndicator, CheckboxBox } from './styles'
3
+ import { Text } from '../Text'
4
+ import { InputBox } from '../InputBox'
5
+ import { ComponentProps } from 'react'
6
+
7
+ export interface CheckboxProps
8
+ extends ComponentProps<typeof CheckboxContainer> {
9
+ label?: string
10
+ error?: string
11
+ }
12
+
13
+ export function Checkbox({ label, error, ...props }: CheckboxProps) {
14
+ return (
15
+ <InputBox error={error}>
16
+ <CheckboxBox>
17
+ <CheckboxContainer {...props} errored={!!error}>
18
+ <CheckboxIndicator asChild>
19
+ <Check weight="bold" />
20
+ </CheckboxIndicator>
21
+ </CheckboxContainer>
22
+ {!!label && <Text size="sm">{label}</Text>}
23
+ </CheckboxBox>
24
+ </InputBox>
25
+ )
26
+ }
27
+
28
+ Checkbox.displayName = 'Checkbox'
@@ -0,0 +1,84 @@
1
+ import { styled, keyframes } from '../../styles'
2
+ import * as Checkbox from '@radix-ui/react-checkbox'
3
+
4
+ export const CheckboxBox = styled('div', {
5
+ display: 'flex',
6
+ gap: '$2',
7
+
8
+ '&:has(button:disabled)': {
9
+ opacity: 0.5,
10
+ },
11
+ })
12
+
13
+ export const CheckboxContainer = styled(Checkbox.Root, {
14
+ all: 'unset',
15
+ width: '$6',
16
+ height: '$6',
17
+ backgroundColor: '$gray900',
18
+ borderRadius: '$xs',
19
+ lineHeight: 0,
20
+ cursor: 'pointer',
21
+ overflow: 'hidden',
22
+ boxSizing: 'border-box',
23
+ display: 'flex',
24
+ justifyContent: 'center',
25
+ alignItems: 'center',
26
+ border: '2px solid $gray900',
27
+
28
+ '&[data-state="checked"]': {
29
+ backgroundColor: '$primary',
30
+ borderColor: '$primary',
31
+ },
32
+
33
+ '&:focus': {
34
+ border: '2px solid $primary',
35
+ },
36
+
37
+ '&:disabled': {
38
+ cursor: 'not-allowed',
39
+ },
40
+
41
+ variants: {
42
+ errored: {
43
+ true: {
44
+ borderColor: '$danger',
45
+ },
46
+ },
47
+ },
48
+
49
+ defaultVariants: {
50
+ errored: false,
51
+ },
52
+ })
53
+
54
+ const slideIn = keyframes({
55
+ from: {
56
+ transform: 'translateX(-100%)',
57
+ },
58
+ to: {
59
+ transform: 'translateX(0)',
60
+ },
61
+ })
62
+
63
+ const slideOut = keyframes({
64
+ from: {
65
+ transform: 'translateX(0)',
66
+ },
67
+ to: {
68
+ transform: 'translateX(100%)',
69
+ },
70
+ })
71
+
72
+ export const CheckboxIndicator = styled(Checkbox.Indicator, {
73
+ color: '$smokedWhite',
74
+ width: '$4',
75
+ height: '$4',
76
+
77
+ '&[data-state="checked"]': {
78
+ animation: `${slideIn} 100ms ease-out`,
79
+ },
80
+
81
+ '&[data-state="unchecked"]': {
82
+ animation: `${slideOut} 100ms ease-out`,
83
+ },
84
+ })
@@ -0,0 +1,98 @@
1
+ import { useRef, useState, useEffect } from 'react'
2
+ import {
3
+ DropdownContainer,
4
+ DropdownList,
5
+ DropdownItem,
6
+ DropdownTrigger,
7
+ } from './styles'
8
+ import { Button } from './../Button'
9
+
10
+ export interface Option {
11
+ text: string
12
+ variant?: 'default' | 'danger'
13
+ onSelect: () => void
14
+ }
15
+
16
+ export interface DropdownProps {
17
+ options: Option[]
18
+ name?: string
19
+ children?: React.ReactElement
20
+ }
21
+
22
+ export function Dropdown({ options, children, name }: DropdownProps) {
23
+ const [isOpen, setIsOpen] = useState(false)
24
+ const dropdownRef = useRef<HTMLUListElement>(null)
25
+ const buttonRef = useRef<HTMLButtonElement>(null)
26
+
27
+ const toggleDropdown = () => {
28
+ setIsOpen(!isOpen)
29
+ }
30
+
31
+ const handleSelectOption = (option: Option) => {
32
+ option.onSelect()
33
+ setIsOpen(false)
34
+ }
35
+
36
+ const handleClickOutside = (event: MouseEvent) => {
37
+ if (
38
+ dropdownRef.current &&
39
+ buttonRef.current &&
40
+ !dropdownRef.current.contains(event.target as Node) &&
41
+ !buttonRef.current.contains(event.target as Node)
42
+ ) {
43
+ setIsOpen(false)
44
+ }
45
+ }
46
+
47
+ useEffect(() => {
48
+ document.addEventListener('mousedown', handleClickOutside)
49
+ return () => {
50
+ document.removeEventListener('mousedown', handleClickOutside)
51
+ }
52
+ }, [])
53
+
54
+ useEffect(() => {
55
+ const handleReposition = () => {
56
+ if (isOpen && buttonRef.current && dropdownRef.current) {
57
+ const buttonRect = buttonRef.current.getBoundingClientRect()
58
+ const dropdownRect = dropdownRef.current.getBoundingClientRect()
59
+ const buttonTop = buttonRect.top
60
+ const buttonHeight = buttonRect.height
61
+ const dropdownHeight = dropdownRect.height
62
+
63
+ const spaceBelow = window.innerHeight - buttonTop - buttonHeight
64
+ if (spaceBelow < dropdownHeight) {
65
+ dropdownRef.current.style.bottom = `${buttonHeight + 6}px`
66
+ } else {
67
+ dropdownRef.current.style.top = `${buttonHeight + 6}px`
68
+ }
69
+ }
70
+ }
71
+
72
+ handleReposition()
73
+ }, [isOpen])
74
+
75
+ return (
76
+ <DropdownContainer>
77
+ <DropdownTrigger ref={buttonRef} onClick={toggleDropdown}>
78
+ {name && children === undefined && <Button>{name}</Button>}
79
+ {children && name === undefined && children}
80
+ </DropdownTrigger>
81
+ {isOpen && (
82
+ <DropdownList ref={dropdownRef}>
83
+ {options.map((option, index) => (
84
+ <DropdownItem
85
+ key={index}
86
+ onClick={() => handleSelectOption(option)}
87
+ variant={option.variant}
88
+ >
89
+ {option.text}
90
+ </DropdownItem>
91
+ ))}
92
+ </DropdownList>
93
+ )}
94
+ </DropdownContainer>
95
+ )
96
+ }
97
+
98
+ Dropdown.displayName = 'Dropdown'
@@ -0,0 +1,75 @@
1
+ import { styled, keyframes } from '../../styles'
2
+
3
+ export const DropdownContainer = styled('div', {
4
+ position: 'relative',
5
+ display: 'inline-block',
6
+ })
7
+
8
+ export const DropdownTrigger = styled('span', {})
9
+
10
+ const fadeIn = keyframes({
11
+ '0%': { opacity: '0' },
12
+ '100%': { opacity: '1' },
13
+ })
14
+
15
+ export const DropdownList = styled('ul', {
16
+ position: 'absolute',
17
+ // top: '100%',
18
+ // left: 0,
19
+ backgroundColor: '#18181B',
20
+ borderRadius: '$lg',
21
+ padding: 0,
22
+ // margin: '$1 0',
23
+ margin: 0,
24
+ listStyle: 'none',
25
+ zIndex: 9999,
26
+ minWidth: '100%', // ensures the dropdown list as wide as the container
27
+
28
+ left: '50%',
29
+ transform: 'translateX(-50%)',
30
+
31
+ animation: `${fadeIn} 100ms ease-out`,
32
+
33
+ '& li:first-child': {
34
+ marginTop: '$2',
35
+ },
36
+ '& li:last-child': {
37
+ marginBottom: '$2',
38
+ },
39
+ })
40
+
41
+ export const DropdownItem = styled('li', {
42
+ fontFamily: '$default',
43
+ fontSize: '$sm',
44
+ color: '$smokedWhite',
45
+ padding: '$2 $2',
46
+ marginLeft: '$2',
47
+ marginRight: '$2',
48
+ width: '$40',
49
+ borderRadius: '$md',
50
+ cursor: 'pointer',
51
+
52
+ variants: {
53
+ variant: {
54
+ default: {
55
+ color: '$smokedWhite',
56
+
57
+ '&:hover': {
58
+ backgroundColor: '$gray700',
59
+ },
60
+ },
61
+ danger: {
62
+ color: '$danger',
63
+
64
+ '&:hover': {
65
+ backgroundColor: '$danger',
66
+ color: '$smokedWhite',
67
+ },
68
+ },
69
+ },
70
+ },
71
+
72
+ defaultVariants: {
73
+ variant: 'default',
74
+ },
75
+ })
@@ -0,0 +1,32 @@
1
+ import { ComponentProps, ElementType } from 'react'
2
+ import { styled } from '../styles'
3
+
4
+ export const Heading = styled('h2', {
5
+ fontFamily: '$heading',
6
+ lineHeight: '$shorter',
7
+ margin: 0,
8
+ color: '$smokedWhite',
9
+
10
+ variants: {
11
+ size: {
12
+ sm: { fontSize: '$xl' },
13
+ md: { fontSize: '$2xl' },
14
+ lg: { fontSize: '$4xl' },
15
+ '2xl': { fontSize: '$5xl' },
16
+ '3xl': { fontSize: '$5xl' },
17
+ '4xl': { fontSize: '$6xl' },
18
+ '5xl': { fontSize: '$7xl' },
19
+ '6xl': { fontSize: '$8xl' },
20
+ },
21
+ },
22
+
23
+ defaultVariants: {
24
+ size: 'md',
25
+ },
26
+ })
27
+
28
+ export interface HeadingProps extends ComponentProps<typeof Heading> {
29
+ as?: ElementType
30
+ }
31
+
32
+ Heading.displayName = 'Heading'
@@ -0,0 +1,24 @@
1
+ import { Text } from '../Text'
2
+ import { InputContainer } from './styles'
3
+
4
+ export interface InputProps {
5
+ label?: string
6
+ children: React.ReactElement
7
+ error?: string
8
+ }
9
+
10
+ export function InputBox({ label, error, children }: InputProps) {
11
+ return (
12
+ <InputContainer>
13
+ {!!label && <Text size={'sm'}>{label}</Text>}
14
+ {children}
15
+ {!!error && (
16
+ <Text size="xs" variant="danger">
17
+ {error}
18
+ </Text>
19
+ )}
20
+ </InputContainer>
21
+ )
22
+ }
23
+
24
+ InputBox.displayName = 'InputBox'
@@ -0,0 +1,7 @@
1
+ import { styled } from '../../styles'
2
+
3
+ export const InputContainer = styled('div', {
4
+ display: 'flex',
5
+ flexDirection: 'column',
6
+ gap: '$2',
7
+ })
@@ -0,0 +1,45 @@
1
+ import { ComponentProps, ElementType } from 'react'
2
+ import { styled } from '../styles'
3
+
4
+ export const Text = styled('p', {
5
+ fontFamily: '$default',
6
+ lineHeight: '$base',
7
+ margin: 0,
8
+ color: '$smokedWhite',
9
+
10
+ variants: {
11
+ size: {
12
+ xxs: { fontSize: '$xxs' },
13
+ xs: { fontSize: '$xs' },
14
+ sm: { fontSize: '$sm' },
15
+ md: { fontSize: '$md' },
16
+ lg: { fontSize: '$lg' },
17
+ xl: { fontSize: '$xl' },
18
+ '2xl': { fontSize: '$2xl' },
19
+ '4xl': { fontSize: '$4xl' },
20
+ '5xl': { fontSize: '$5xl' },
21
+ '6xl': { fontSize: '$6xl' },
22
+ '7xl': { fontSize: '$7xl' },
23
+ '8xl': { fontSize: '$8xl' },
24
+ '9xl': { fontSize: '$9xl' },
25
+ },
26
+ variant: {
27
+ default: { color: '$smokedWhite' },
28
+ primary: { color: '$primary' },
29
+ danger: { color: '$danger' },
30
+ success: { color: '$success' },
31
+ warning: { color: '$warning' },
32
+ },
33
+ },
34
+
35
+ defaultVariants: {
36
+ size: 'md',
37
+ variant: 'default',
38
+ },
39
+ })
40
+
41
+ export interface TextProps extends ComponentProps<typeof Text> {
42
+ as?: ElementType
43
+ }
44
+
45
+ Text.displayName = 'Text'
@@ -0,0 +1,18 @@
1
+ import { ComponentProps } from 'react'
2
+ import { InputBox } from '../InputBox'
3
+ import { TextAreaInput } from './styles'
4
+
5
+ export interface TextAreaProps extends ComponentProps<typeof TextAreaInput> {
6
+ label?: string
7
+ error?: string
8
+ }
9
+
10
+ export function TextArea({ label, error, ...props }: TextAreaProps) {
11
+ return (
12
+ <InputBox label={label} error={error}>
13
+ <TextAreaInput {...props} errored={!!error} />
14
+ </InputBox>
15
+ )
16
+ }
17
+
18
+ TextArea.displayName = 'TextArea'
@@ -0,0 +1,42 @@
1
+ import { styled } from '../../styles'
2
+
3
+ export const TextAreaInput = styled('textarea', {
4
+ backgroundColor: '$gray900',
5
+ padding: '$3 $4',
6
+ borderRadius: '$sm',
7
+ boxSizing: 'border-box',
8
+ border: '2px solid $gray900',
9
+
10
+ fontFamily: '$default',
11
+ fontSize: '$sm',
12
+ color: '$white',
13
+ fontWeight: '$regular',
14
+ resize: 'vertical',
15
+ minHeight: 80,
16
+
17
+ '&:focus': {
18
+ outline: 0,
19
+ borderColor: '$primary',
20
+ },
21
+
22
+ '&:disabled': {
23
+ opacity: 0.5,
24
+ cursor: 'not-allowed',
25
+ },
26
+
27
+ '&:placeholder': {
28
+ color: '$gray400',
29
+ },
30
+
31
+ variants: {
32
+ errored: {
33
+ true: {
34
+ borderColor: '$danger',
35
+ },
36
+ },
37
+ },
38
+
39
+ defaultVariants: {
40
+ errored: false,
41
+ },
42
+ })
@@ -0,0 +1,22 @@
1
+ import { ComponentProps } from 'react'
2
+ import { Input, Prefix, TextInputContainer } from './styles'
3
+ import { InputBox } from '../InputBox'
4
+
5
+ export interface TextInputProps extends ComponentProps<typeof Input> {
6
+ label?: string
7
+ prefix?: string
8
+ error?: string
9
+ }
10
+
11
+ export function TextInput({ label, prefix, error, ...props }: TextInputProps) {
12
+ return (
13
+ <InputBox label={label} error={error}>
14
+ <TextInputContainer errored={!!error}>
15
+ {!!prefix && <Prefix>{prefix}</Prefix>}
16
+ <Input {...props} />
17
+ </TextInputContainer>
18
+ </InputBox>
19
+ )
20
+ }
21
+
22
+ TextInput.displayName = 'TextInput'