diff --git a/.gitignore b/.gitignore index b991ff9..55caa5b 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,3 @@ cache/ *.log *.debug -# Generated Components -components/ -!packages/components/**/* diff --git a/components/.gitignore b/components/.gitignore new file mode 100644 index 0000000..91dfed8 --- /dev/null +++ b/components/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +node_modules \ No newline at end of file diff --git a/components/.npmignore b/components/.npmignore new file mode 100644 index 0000000..eb5b7ff --- /dev/null +++ b/components/.npmignore @@ -0,0 +1,2 @@ +src/ +test/ \ No newline at end of file diff --git a/packages/components/jest.config.ts b/components/jest.config.ts similarity index 78% rename from packages/components/jest.config.ts rename to components/jest.config.ts index 49729d6..02c0206 100644 --- a/packages/components/jest.config.ts +++ b/components/jest.config.ts @@ -1,7 +1,7 @@ -import type { Config } from '@jest/types'; -import { pathsToModuleNameMapper } from 'ts-jest/utils'; +import type { Config } from '@jest/types' +import { pathsToModuleNameMapper } from 'ts-jest/utils' -import { compilerOptions } from './tsconfig.json'; +import { compilerOptions } from './tsconfig.json' const config: Config.InitialOptions = { collectCoverageFrom: [ @@ -30,6 +30,6 @@ const config: Config.InitialOptions = { 'jest-watch-typeahead/filename', 'jest-watch-typeahead/testname', ], -}; +} -export default config; +export default config diff --git a/packages/components/package.json b/components/package.json similarity index 60% rename from packages/components/package.json rename to components/package.json index bb519a8..3800f3e 100644 --- a/packages/components/package.json +++ b/components/package.json @@ -1,7 +1,7 @@ { "name": "@disco3/components", - "version": "0.0.1", - "description": "web3 design system for disco3", + "version": "0.0.8", + "description": "Design system for Disco3", "main": "./dist/index.cjs.js", "module": "./dist/index.es.js", "types": "./dist/types/index.d.ts", @@ -23,51 +23,27 @@ "files": [ "dist/**" ], + "repository": "manifoldfinance/disco3-react", "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/manifoldfinance/disco3-react.git", - "directory": "packages/components" - }, - "keywords": [ - "react", - "typescript", - "bundle", - "ethereum", - "web3", - "vanilla", - "react", - "components" - ], - "bugs": { - "url": "https://github.com/manifoldfinance/disco3-react/issues" - }, "scripts": { - "build": "NODE_ENV=production npx vite build", + "build": "vite build", "clean": "rimraf dist src/components/icons/generated/", - "lint:types": "npx tsc --noEmit", - "prepack": "yarn build", + "lint:types": "tsc --noEmit", + "build-pack": "yarn gen:icons && yarn build", "publish": "yarn npm publish", - "test": "npx jest -u" + "test": "jest" }, "dependencies": { "@vanilla-extract/css": "^1.6.8", "@vanilla-extract/css-utils": "^0.1.2", "@vanilla-extract/dynamic": "^2.0.2", - "@vanilla-extract/recipes": "^0.2.3", - "@vanilla-extract/sprinkles": "^1.3.3", + "@vanilla-extract/recipes": "^0.2.4", + "@vanilla-extract/sprinkles": "^1.4.0", "clsx": "^1.1.1", "focus-visible": "^5.2.0" }, "devDependencies": { - "@babel/core": "7.4.5", - "@babel/plugin-proposal-class-properties": "7.4.4", - "@babel/preset-env": "7.4.5", - "@babel/preset-react": "7.0.0", - "@babel/preset-typescript": "7.3.3", - "@babel/register": "7.4.4", "@jest/types": "^27.2.5", - "@svgr/babel-plugin-remove-jsx-attribute": "^6.0.0", "@svgr/core": "^6.2.1", "@svgr/plugin-jsx": "^6.2.1", "@svgr/plugin-prettier": "^6.1.2", @@ -77,16 +53,16 @@ "@testing-library/react": "^12.1.2", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.5.0", - "@types/testing-library__jest-dom": "^5.14.1", "@types/glob": "^7.2.0", "@types/node": "^16.11.6", "@types/react": "^17.0.33", "@types/react-dom": "^17.0.10", "@types/rimraf": "^3.0.2", - "@vanilla-extract/babel-plugin": "^1.1.4", - "@vanilla-extract/private": "^1.0.3", - "@vanilla-extract/vite-plugin": "^3.1.2", + "@types/testing-library__jest-dom": "^5.14.1", + "@vanilla-extract/private": "^1.0.1", + "@vanilla-extract/vite-plugin": "^3.0.0", "change-case": "^4.1.2", + "dedent": "^0.7.0", "deepmerge": "^4.2.2", "glob": "^7.2.0", "jest": "^27.3.1", @@ -96,9 +72,10 @@ "rimraf": "^3.0.2", "ts-jest": "^27.0.7", "ts-node": "^10.4.0", - "vite": "^2.6.13", + "typescript": "4.6.3", + "vite": "^2.9.1", "vite-plugin-dts": "^0.9.2", - "vite-tsconfig-paths": "^3.3.17" + "vite-tsconfig-paths": "^3.4.1" }, "peerDependencies": { "react": "^17", diff --git a/packages/components/src/components/Avatar/Avatar.docs.mdx b/components/src/components/Avatar/Avatar.docs.mdx similarity index 65% rename from packages/components/src/components/Avatar/Avatar.docs.mdx rename to components/src/components/Avatar/Avatar.docs.mdx index 57d0f11..445ebfb 100644 --- a/packages/components/src/components/Avatar/Avatar.docs.mdx +++ b/components/src/components/Avatar/Avatar.docs.mdx @@ -4,7 +4,7 @@ description: Representation of entity --- ```tsx -import { Avatar } from '@disco3/components'; +import { Avatar } from 'degen' ``` ```tsx live=true expand=true @@ -53,9 +53,7 @@ import { Avatar } from '@disco3/components'; ## Border -By default, there is a light border around the edge to define image edges on -similarly colored backgrounds. This can be disabled by using the `noBorder` -prop. +By default, there is a light border around the edge to define image edges on similarly colored backgrounds. This can be disabled by using the `noBorder` prop. ```tsx live=true @@ -63,8 +61,8 @@ prop. size="24" src={ mode === 'light' - ? 'https://d.pr/i/2532ep.jpeg' - : 'https://d.pr/i/QQNPxB.jpegg' + ? 'https://images.mirror-media.xyz/publication-images/152cd86f-45ad-4f48-8971-4dd33fde5bc1.png' + : 'https://images.mirror-media.xyz/publication-images/5eaba6b2-b9ee-4bea-aeac-75796d53798f.jpg' } /> @@ -81,9 +79,7 @@ prop. ## next/image -If you are using Next.js and configured -[`next/image`](https://nextjs.org/docs/api-reference/next/image) appropriately, -you can pass it in with the `as` prop. +If you are using Next.js and configured [`next/image`](https://nextjs.org/docs/api-reference/next/image) appropriately, you can pass it in with the `as` prop. ```tsx live=true expand=true diff --git a/components/src/components/Avatar/Avatar.snippets.tsx b/components/src/components/Avatar/Avatar.snippets.tsx new file mode 100644 index 0000000..61c85c0 --- /dev/null +++ b/components/src/components/Avatar/Avatar.snippets.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { Avatar } from './Avatar' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: ( + + ), + }, +] diff --git a/components/src/components/Avatar/Avatar.test.tsx b/components/src/components/Avatar/Avatar.test.tsx new file mode 100644 index 0000000..3847506 --- /dev/null +++ b/components/src/components/Avatar/Avatar.test.tsx @@ -0,0 +1,19 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Avatar } from './Avatar' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render( + , + ) + expect(screen.getByRole(/img/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/Avatar/Avatar.tsx b/components/src/components/Avatar/Avatar.tsx similarity index 76% rename from packages/components/src/components/Avatar/Avatar.tsx rename to components/src/components/Avatar/Avatar.tsx index 8093f73..6200d88 100644 --- a/packages/components/src/components/Avatar/Avatar.tsx +++ b/components/src/components/Avatar/Avatar.tsx @@ -1,18 +1,17 @@ -import * as React from 'react'; -import * as styles from './styles.css'; +import * as React from 'react' -import { Box, BoxProps } from '../Box'; - -import { UserSolid } from '../disco-icons'; +import { Box, BoxProps } from '../Box' +import { IconUserSolid } from '../icons' +import * as styles from './styles.css' export type Props = { - as?: 'img' | React.ComponentType; - label: string; - placeholder?: boolean; - noBorder?: boolean; - size?: BoxProps['height']; - src?: string; -} & styles.Variants; + as?: 'img' | React.ComponentType + label: string + placeholder?: boolean + noBorder?: boolean + size?: BoxProps['height'] + src?: string +} & styles.Variants export const Avatar = ({ as = 'img', @@ -62,5 +61,5 @@ export const Avatar = ({ )} - ); -}; + ) +} diff --git a/components/src/components/Avatar/index.ts b/components/src/components/Avatar/index.ts new file mode 100644 index 0000000..d59bf3b --- /dev/null +++ b/components/src/components/Avatar/index.ts @@ -0,0 +1,2 @@ +export { Avatar } from './Avatar' +export type { Props } from './Avatar' diff --git a/packages/components/src/components/Avatar/styles.css.ts b/components/src/components/Avatar/styles.css.ts similarity index 84% rename from packages/components/src/components/Avatar/styles.css.ts rename to components/src/components/Avatar/styles.css.ts index 3f7cb7d..1f5d094 100644 --- a/packages/components/src/components/Avatar/styles.css.ts +++ b/components/src/components/Avatar/styles.css.ts @@ -1,7 +1,7 @@ -import { style } from '@vanilla-extract/css'; -import { RecipeVariants, recipe } from '@vanilla-extract/recipes'; +import { style } from '@vanilla-extract/css' +import { RecipeVariants, recipe } from '@vanilla-extract/recipes' -import { atoms, vars } from '../../css'; +import { atoms, vars } from '../../css' export const variants = recipe({ variants: { @@ -42,6 +42,6 @@ export const variants = recipe({ defaultVariants: { noBorder: false, }, -}); +}) -export type Variants = RecipeVariants; +export type Variants = RecipeVariants diff --git a/components/src/components/AvatarGroup/AvatarGroup.docs.mdx b/components/src/components/AvatarGroup/AvatarGroup.docs.mdx new file mode 100644 index 0000000..e978316 --- /dev/null +++ b/components/src/components/AvatarGroup/AvatarGroup.docs.mdx @@ -0,0 +1,71 @@ +--- +title: AvatarGroup +description: Group of entity representations +--- + +```tsx +import { AvatarGroup } from 'degen' +``` + +```tsx live=true expand=true + +``` + +## Props + +See [`Avatar`'s props](/components/Avatar#props) for more info. + + + +## Sizes + +```tsx live=true + + + + + +``` + +## Limit + +```tsx live=true expand=true + +``` diff --git a/components/src/components/AvatarGroup/AvatarGroup.snippets.tsx b/components/src/components/AvatarGroup/AvatarGroup.snippets.tsx new file mode 100644 index 0000000..4dcdd02 --- /dev/null +++ b/components/src/components/AvatarGroup/AvatarGroup.snippets.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { AvatarGroup } from './AvatarGroup' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: ( + + ), + }, +] diff --git a/components/src/components/AvatarGroup/AvatarGroup.test.tsx b/components/src/components/AvatarGroup/AvatarGroup.test.tsx new file mode 100644 index 0000000..f62b8ae --- /dev/null +++ b/components/src/components/AvatarGroup/AvatarGroup.test.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { AvatarGroup } from './AvatarGroup' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render( + , + ) + expect(screen.getAllByRole(/img/i).length).toBe(3) + }) +}) diff --git a/packages/components/src/components/AvatarGroup/AvatarGroup.tsx b/components/src/components/AvatarGroup/AvatarGroup.tsx similarity index 51% rename from packages/components/src/components/AvatarGroup/AvatarGroup.tsx rename to components/src/components/AvatarGroup/AvatarGroup.tsx index ece556d..bca73e7 100644 --- a/packages/components/src/components/AvatarGroup/AvatarGroup.tsx +++ b/components/src/components/AvatarGroup/AvatarGroup.tsx @@ -1,19 +1,20 @@ -import * as React from 'react'; +import * as React from 'react' -import { Avatar, Props as AvatarProps } from '../Avatar'; -import { Box, BoxProps } from '../Box'; -import * as styles from './styles.css'; +import { Avatar, Props as AvatarProps } from '../Avatar' +import { Box, BoxProps } from '../Box' +import { useTheme } from '../ThemeProvider' +import * as styles from './styles.css' type Props = { - as?: 'img' | React.ComponentType; - limit?: number; + as?: 'img' | React.ComponentType + limit?: number members: { - label: AvatarProps['label']; - placeholder?: AvatarProps['placeholder']; - src?: AvatarProps['src']; - }[]; - size?: BoxProps['height']; -}; + label: AvatarProps['label'] + placeholder?: AvatarProps['placeholder'] + src?: AvatarProps['src'] + }[] + size?: BoxProps['height'] +} export const AvatarGroup = ({ as, @@ -21,8 +22,10 @@ export const AvatarGroup = ({ members = [], size = '6', }: Props) => { - const membersCount = members.length; - const visibleMembers = members.slice(0, limit); + const { mode, forcedMode } = useTheme() + const membersCount = members.length + const visibleMembers = members.slice(0, limit) + const variantSize = size < 5 ? 'small' : 'large' return ( @@ -32,7 +35,7 @@ export const AvatarGroup = ({ borderRadius="full" className={styles.wrapper} key={x.label} - marginLeft={i === 0 ? '0' : '-1.5'} + marginLeft={i === 0 ? '0' : variantSize === 'small' ? '-1' : '-1.5'} > {membersCount > limit && ( +{(membersCount - limit).toLocaleString()} )} - ); -}; + ) +} diff --git a/components/src/components/AvatarGroup/index.ts b/components/src/components/AvatarGroup/index.ts new file mode 100644 index 0000000..d52d2c9 --- /dev/null +++ b/components/src/components/AvatarGroup/index.ts @@ -0,0 +1 @@ +export { AvatarGroup } from './AvatarGroup' diff --git a/components/src/components/AvatarGroup/styles.css.ts b/components/src/components/AvatarGroup/styles.css.ts new file mode 100644 index 0000000..4b8107d --- /dev/null +++ b/components/src/components/AvatarGroup/styles.css.ts @@ -0,0 +1,61 @@ +import { style } from '@vanilla-extract/css' +import { recipe } from '@vanilla-extract/recipes' + +import { atoms, vars } from '../../css' + +export const wrapper = style({ + boxShadow: `0 0 0 2px ${vars.colors.background}`, + transform: `translateX(0px)`, +}) + +export const overflowText = recipe({ + variants: { + size: { + small: [ + atoms({ + alignSelf: 'stretch', + position: 'relative', + display: 'flex', + alignItems: 'center', + borderRadius: 'full', + }), + wrapper, + { + fontSize: 8, + letterSpacing: '-0.009em', + paddingLeft: '0.33rem', + paddingRight: '0.33rem', + marginLeft: '-0.1875rem', + }, + ], + large: atoms({ + fontSize: 'base', + marginLeft: '1.5', + }), + }, + theme: { + light: {}, + dark: {}, + }, + }, + compoundVariants: [ + { + variants: { + size: 'small', + theme: 'light', + }, + style: { + backgroundColor: '#ECECEC', + }, + }, + { + variants: { + size: 'small', + theme: 'dark', + }, + style: { + backgroundColor: '#262626', + }, + }, + ], +}) diff --git a/packages/components/src/components/Box/Box.docs.mdx b/components/src/components/Box/Box.docs.mdx similarity index 80% rename from packages/components/src/components/Box/Box.docs.mdx rename to components/src/components/Box/Box.docs.mdx index 2577e71..37e0a26 100644 --- a/packages/components/src/components/Box/Box.docs.mdx +++ b/components/src/components/Box/Box.docs.mdx @@ -4,7 +4,7 @@ description: Primitive for consuming design tokens --- ```tsx -import { Box } from '@disco3/components'; +import { Box } from 'degen' ``` ```tsx expand=true live=true @@ -13,7 +13,6 @@ import { Box } from '@disco3/components'; ## Props -`React.AllHTMLAttributes` properties are supported, but not listed below for -brevity. +`React.AllHTMLAttributes` properties are supported, but not listed below for brevity. diff --git a/components/src/components/Box/Box.snippets.tsx b/components/src/components/Box/Box.snippets.tsx new file mode 100644 index 0000000..5b0928f --- /dev/null +++ b/components/src/components/Box/Box.snippets.tsx @@ -0,0 +1,12 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' + +import { Box } from './Box' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: _, + }, +] diff --git a/components/src/components/Box/Box.test.tsx b/components/src/components/Box/Box.test.tsx new file mode 100644 index 0000000..9bfa9b9 --- /dev/null +++ b/components/src/components/Box/Box.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Box } from './Box' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render(foo bar baz) + expect(screen.getByText(/foo/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/Box/Box.tsx b/components/src/components/Box/Box.tsx similarity index 56% rename from packages/components/src/components/Box/Box.tsx rename to components/src/components/Box/Box.tsx index d926d57..b58d254 100644 --- a/packages/components/src/components/Box/Box.tsx +++ b/components/src/components/Box/Box.tsx @@ -1,45 +1,45 @@ -import * as React from 'react'; -import clsx, { ClassValue } from 'clsx'; +import * as React from 'react' +import clsx, { ClassValue } from 'clsx' -import { Atoms, atoms, sprinkles } from '../../css'; +import { Atoms, atoms, sprinkles } from '../../css' type HTMLProperties = Omit< React.AllHTMLAttributes, 'as' | 'className' | 'color' | 'height' | 'width' ->; +> type Props = Atoms & HTMLProperties & { - as?: React.ElementType; - className?: ClassValue; - }; + as?: React.ElementType + className?: ClassValue + } export const Box = React.forwardRef( ({ as = 'div', className, ...props }: Props, ref) => { - const atomProps: Record = {}; - const nativeProps: Record = {}; + const atomProps: Record = {} + const nativeProps: Record = {} for (const key in props) { if (sprinkles.properties.has(key as keyof Omit)) { - atomProps[key] = props[key as keyof typeof props]; + atomProps[key] = props[key as keyof typeof props] } else { - nativeProps[key] = props[key as keyof typeof props]; + nativeProps[key] = props[key as keyof typeof props] } } const atomicClasses = atoms({ reset: typeof as === 'string' ? (as as Atoms['reset']) : 'div', ...atomProps, - }); + }) return React.createElement(as, { className: clsx(atomicClasses, className), ...nativeProps, ref, - }); + }) }, -); +) -export type BoxProps = Parameters[0]; +export type BoxProps = Parameters[0] -Box.displayName = 'Box'; +Box.displayName = 'Box' diff --git a/components/src/components/Box/index.ts b/components/src/components/Box/index.ts new file mode 100644 index 0000000..6067699 --- /dev/null +++ b/components/src/components/Box/index.ts @@ -0,0 +1,2 @@ +export { Box } from './Box' +export type { BoxProps } from './Box' diff --git a/packages/components/src/components/Button/Button.docs.mdx b/components/src/components/Button/Button.docs.mdx similarity index 92% rename from packages/components/src/components/Button/Button.docs.mdx rename to components/src/components/Button/Button.docs.mdx index e04f4c7..ec92c4b 100644 --- a/packages/components/src/components/Button/Button.docs.mdx +++ b/components/src/components/Button/Button.docs.mdx @@ -4,7 +4,7 @@ description: Trigger for actions --- ```tsx -import { Button } from '@disco3/components'; +import { Button } from 'degen' ``` ```tsx expand=true live=true @@ -36,8 +36,8 @@ import { Button } from '@disco3/components'; disabled={getState('loading')} loading={getState('loading')} onClick={() => { - toggleState('loading'); - setTimeout(() => toggleState('loading'), 1000); + toggleState('loading') + setTimeout(() => toggleState('loading'), 1000) }} > Click Me @@ -49,6 +49,7 @@ import { Button } from '@disco3/components'; ```tsx live=true + @@ -56,8 +57,7 @@ import { Button } from '@disco3/components'; ## Tones -When `variant` is set to `primary` or `secondary`, you can `tone` to change the -button accent color. +When `variant` is set to `primary` or `secondary`, you can `tone` to change the button accent color. ```tsx live=true @@ -172,13 +172,8 @@ Optional `center` prop reserves space for affixes and loading spinner. ```tsx live=true - diff --git a/components/src/components/Button/Button.snippets.tsx b/components/src/components/Button/Button.snippets.tsx new file mode 100644 index 0000000..ae263f0 --- /dev/null +++ b/components/src/components/Button/Button.snippets.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { Button } from './Button' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: , + }, +] diff --git a/components/src/components/Button/Button.test.tsx b/components/src/components/Button/Button.test.tsx new file mode 100644 index 0000000..5c1432c --- /dev/null +++ b/components/src/components/Button/Button.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Button } from './Button' + +describe(') + expect(screen.getByText(/connect/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/Button/Button.tsx b/components/src/components/Button/Button.tsx similarity index 70% rename from packages/components/src/components/Button/Button.tsx rename to components/src/components/Button/Button.tsx index 12fa3ee..76d3c9d 100644 --- a/packages/components/src/components/Button/Button.tsx +++ b/components/src/components/Button/Button.tsx @@ -1,26 +1,26 @@ -import * as React from 'react'; +import * as React from 'react' -import { ReactNodeNoStrings } from '../../types'; -import { Box, BoxProps } from '../Box'; -import { Spinner } from '../Spinner'; -import { Text } from '../Text'; -import { getCenterProps } from './utils'; -import * as styles from './styles.css'; +import { ReactNodeNoStrings } from '../../types' +import { Box, BoxProps } from '../Box' +import { Spinner } from '../Spinner' +import { Text } from '../Text' +import { getCenterProps } from './utils' +import * as styles from './styles.css' type BaseProps = { /** Centers text and reserves space for icon and spinner */ - center?: boolean; + center?: boolean /** Adds ReactNode before children */ - prefix?: ReactNodeNoStrings; + prefix?: ReactNodeNoStrings /** Shows loading spinner inside button */ - loading?: boolean; + loading?: boolean /** Constrains button to specific shape */ - shape?: styles.Shape; + shape?: styles.Shape /** Sets dimensions and layout */ - size?: styles.Size; + size?: styles.Size /** Adds ReactNode after children */ - suffix?: ReactNodeNoStrings; - variant?: styles.Variant; + suffix?: ReactNodeNoStrings + variant?: styles.Variant } & Pick< JSX.IntrinsicElements['button'], | 'onClick' @@ -31,29 +31,29 @@ type BaseProps = { | 'type' | 'tabIndex' > & - Pick; + Pick type WithTone = { - tone?: styles.Tone; - variant?: 'primary' | 'secondary'; -}; + tone?: styles.Tone + variant?: 'primary' | 'secondary' +} type WithoutTone = { - tone?: never; - variant?: styles.Variant; -}; + tone?: never + variant?: styles.Variant +} type WithAnchor = { - as?: 'a'; -} & Pick; + as?: 'a' +} & Pick type WithoutAnchor = { - as?: 'button'; -}; + as?: 'button' +} export type Props = BaseProps & (WithTone | WithoutTone) & - (WithAnchor | WithoutAnchor); + (WithAnchor | WithoutAnchor) export const Button = React.forwardRef( ( @@ -63,7 +63,7 @@ export const Button = React.forwardRef( prefix, loading, shape, - size = 'medium', + size = 'large', suffix, tone = 'accent', variant = 'primary', @@ -75,11 +75,11 @@ export const Button = React.forwardRef( {children} - ); + ) - let childContent: ReactNodeNoStrings; + let childContent: ReactNodeNoStrings if (shape) { - childContent = loading ? : labelContent; + childContent = loading ? : labelContent } else { childContent = ( <> @@ -95,7 +95,7 @@ export const Button = React.forwardRef( )} - ); + ) } return ( @@ -119,8 +119,8 @@ export const Button = React.forwardRef( > {childContent} - ); + ) }, -); +) -Button.displayName = 'Button'; +Button.displayName = 'Button' diff --git a/components/src/components/Button/index.ts b/components/src/components/Button/index.ts new file mode 100644 index 0000000..eaad758 --- /dev/null +++ b/components/src/components/Button/index.ts @@ -0,0 +1,2 @@ +export { Button } from './Button' +export type { Props } from './Button' diff --git a/packages/components/src/components/Button/styles.css.ts b/components/src/components/Button/styles.css.ts similarity index 85% rename from packages/components/src/components/Button/styles.css.ts rename to components/src/components/Button/styles.css.ts index 7582144..eddfa1d 100644 --- a/packages/components/src/components/Button/styles.css.ts +++ b/components/src/components/Button/styles.css.ts @@ -1,17 +1,17 @@ -import { createVar, style } from '@vanilla-extract/css'; -import { CSSVarFunction } from '@vanilla-extract/private'; -import { RecipeVariants, recipe } from '@vanilla-extract/recipes'; +import { createVar, style } from '@vanilla-extract/css' +import { CSSVarFunction } from '@vanilla-extract/private' +import { RecipeVariants, recipe } from '@vanilla-extract/recipes' -import { atoms, rgb, vars } from '../../css'; +import { atoms, rgb, vars } from '../../css' const shape = { circle: atoms({ borderRadius: 'full', }), square: {}, -}; +} -export type Shape = keyof typeof shape; +export type Shape = keyof typeof shape const size = { small: atoms({ @@ -19,16 +19,25 @@ const size = { fontSize: 'small', height: '10', paddingX: '4', + gap: '2', }), medium: atoms({ + borderRadius: 'large', + fontSize: 'small', + height: '12', + paddingX: '4', + gap: '3', + }), + large: atoms({ borderRadius: '2xLarge', fontSize: 'base', height: '14', paddingX: '5', + gap: '4', }), -}; +} -export type Size = keyof typeof size; +export type Size = keyof typeof size const getAccentVars = (colorVar: CSSVarFunction) => ({ [vars.colors.accent]: rgb(colorVar), @@ -41,7 +50,7 @@ const getAccentVars = (colorVar: CSSVarFunction) => ({ colorVar, vars.mode.shades.accentSecondaryHover, ), -}); +}) const tone = { accent: {}, @@ -60,11 +69,11 @@ const tone = { pink: style({ vars: getAccentVars(vars.mode.colors.pink), }), -}; +} -export type Tone = keyof typeof tone; +export type Tone = keyof typeof tone -const boxShadowColorVar = createVar(); +const boxShadowColorVar = createVar() const variant = { primary: style([ @@ -158,9 +167,9 @@ const variant = { }, }), ]), -}; +} -export type Variant = keyof typeof variant; +export type Variant = keyof typeof variant const getShapeSizeCompoundVariant = (shape: Shape, size: Size) => ({ variants: { @@ -171,17 +180,17 @@ const getShapeSizeCompoundVariant = (shape: Shape, size: Size) => ({ atoms({ borderRadius: shape === 'square' - ? size === 'small' + ? size === 'small' || size === 'medium' ? 'large' : '2xLarge' : undefined, - minWidth: size === 'small' ? '10' : '14', + minWidth: size === 'small' || size === 'medium' ? '10' : '14', }), style({ padding: 0, }), ]), -}); +}) export const variants = recipe({ base: style([ @@ -189,7 +198,6 @@ export const variants = recipe({ alignItems: 'center', cursor: 'pointer', display: 'flex', - gap: '4', transitionDuration: '150', transitionProperty: 'default', transitionTimingFunction: 'inOut', @@ -231,15 +239,17 @@ export const variants = recipe({ }, compoundVariants: [ // Shape + Size + getShapeSizeCompoundVariant('circle', 'large'), getShapeSizeCompoundVariant('circle', 'medium'), getShapeSizeCompoundVariant('circle', 'small'), + getShapeSizeCompoundVariant('square', 'large'), getShapeSizeCompoundVariant('square', 'medium'), getShapeSizeCompoundVariant('square', 'small'), // Center + Size { variants: { center: true, - size: 'medium', + size: 'large', }, style: atoms({ paddingX: '14', @@ -248,13 +258,13 @@ export const variants = recipe({ { variants: { center: true, - size: 'medium', + size: 'large', }, style: atoms({ paddingX: '15', }), }, ], -}); +}) -export type Variants = RecipeVariants; +export type Variants = RecipeVariants diff --git a/packages/components/src/components/Button/utils.test.ts b/components/src/components/Button/utils.test.ts similarity index 64% rename from packages/components/src/components/Button/utils.test.ts rename to components/src/components/Button/utils.test.ts index 134f535..622ba91 100644 --- a/packages/components/src/components/Button/utils.test.ts +++ b/components/src/components/Button/utils.test.ts @@ -1,16 +1,17 @@ -import { getCenterProps } from './utils'; +import { getCenterProps } from './utils' describe.each` center | size | side | expected - ${true} | ${'large'} | ${'left'} | ${{ position: 'absolute', left: 5 }} - ${true} | ${'large'} | ${'right'} | ${{ position: 'absolute', right: 5 }} - ${true} | ${'medium'} | ${'right'} | ${{ position: 'absolute', right: 4 }} - ${undefined} | ${'large'} | ${'left'} | ${{}} + ${true} | ${'small'} | ${'left'} | ${{ position: 'absolute', left: 5 }} + ${true} | ${'small'} | ${'right'} | ${{ position: 'absolute', right: 5 }} + ${true} | ${'medium'} | ${'right'} | ${{ position: 'absolute', right: 5 }} + ${true} | ${'large'} | ${'right'} | ${{ position: 'absolute', right: 4 }} + ${undefined} | ${'small'} | ${'left'} | ${{}} `( 'getCenterProps($center, $size, $side)', ({ center, size, side, expected }) => { it(`returns ${JSON.stringify(expected)}`, () => { - expect(getCenterProps(center, size, side)).toEqual(expected); - }); + expect(getCenterProps(center, size, side)).toEqual(expected) + }) }, -); +) diff --git a/packages/components/src/components/Button/utils.ts b/components/src/components/Button/utils.ts similarity index 54% rename from packages/components/src/components/Button/utils.ts rename to components/src/components/Button/utils.ts index 20f43b3..0bc5e3f 100644 --- a/packages/components/src/components/Button/utils.ts +++ b/components/src/components/Button/utils.ts @@ -1,15 +1,15 @@ -import { BoxProps } from '../Box'; -import { Size } from './styles.css'; +import { BoxProps } from '../Box' +import { Size } from './styles.css' export const getCenterProps = ( center: boolean | undefined, size: Size, side: 'left' | 'right', ): Pick => { - if (!center) return {}; + if (!center) return {} const props: BoxProps = { position: 'absolute', - [side]: size === 'medium' ? 4 : 5, - }; - return props; -}; + [side]: size === 'large' ? 4 : 5, + } + return props +} diff --git a/packages/components/src/components/Card/Card.docs.mdx b/components/src/components/Card/Card.docs.mdx similarity index 94% rename from packages/components/src/components/Card/Card.docs.mdx rename to components/src/components/Card/Card.docs.mdx index 323bb8b..e0428b9 100644 --- a/packages/components/src/components/Card/Card.docs.mdx +++ b/components/src/components/Card/Card.docs.mdx @@ -4,7 +4,7 @@ description: Styled wrapper for content --- ```tsx -import { Card } from '@disco3/components'; +import { Card } from 'degen' ``` ```tsx live=true expand=true backgroundColor=foregroundTertiary diff --git a/components/src/components/Card/Card.snippets.tsx b/components/src/components/Card/Card.snippets.tsx new file mode 100644 index 0000000..0d0319d --- /dev/null +++ b/components/src/components/Card/Card.snippets.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { Card } from './Card' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: _, + }, +] diff --git a/components/src/components/Card/Card.test.tsx b/components/src/components/Card/Card.test.tsx new file mode 100644 index 0000000..ea61fa3 --- /dev/null +++ b/components/src/components/Card/Card.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Card } from './Card' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render(foo bar baz) + expect(screen.getByText(/foo/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/Card/Card.tsx b/components/src/components/Card/Card.tsx similarity index 55% rename from packages/components/src/components/Card/Card.tsx rename to components/src/components/Card/Card.tsx index 7bb4942..2dcb465 100644 --- a/packages/components/src/components/Card/Card.tsx +++ b/components/src/components/Card/Card.tsx @@ -1,18 +1,18 @@ -import * as React from 'react'; +import * as React from 'react' -import { Box, BoxProps } from '../Box'; -import { useTheme } from '../ThemeProvider'; -import * as styles from './styles.css'; +import { Box, BoxProps } from '../Box' +import { useTheme } from '../ThemeProvider' +import * as styles from './styles.css' type Props = { - as?: BoxProps['as']; - shadow?: boolean; - hover?: boolean; - level?: '1' | '2'; - borderRadius?: BoxProps['borderRadius']; - padding?: BoxProps['padding']; - width?: BoxProps['width']; -}; + as?: BoxProps['as'] + shadow?: boolean + hover?: boolean + level?: '1' | '2' + borderRadius?: BoxProps['borderRadius'] + padding?: BoxProps['padding'] + width?: BoxProps['width'] +} export const Card = ({ as = 'div', @@ -24,7 +24,7 @@ export const Card = ({ borderRadius = { xs: '2xLarge', sm: '3xLarge' }, width, }: React.PropsWithChildren) => { - const { mode, forcedMode } = useTheme(); + const { mode, forcedMode } = useTheme() return ( {children} - ); -}; + ) +} diff --git a/components/src/components/Card/index.ts b/components/src/components/Card/index.ts new file mode 100644 index 0000000..423e583 --- /dev/null +++ b/components/src/components/Card/index.ts @@ -0,0 +1 @@ +export { Card } from './Card' diff --git a/packages/components/src/components/Card/styles.css.ts b/components/src/components/Card/styles.css.ts similarity index 88% rename from packages/components/src/components/Card/styles.css.ts rename to components/src/components/Card/styles.css.ts index 0841def..e6019b3 100644 --- a/packages/components/src/components/Card/styles.css.ts +++ b/components/src/components/Card/styles.css.ts @@ -1,9 +1,9 @@ -import { RecipeVariants, recipe } from '@vanilla-extract/recipes'; -import { createVar, style } from '@vanilla-extract/css'; +import { RecipeVariants, recipe } from '@vanilla-extract/recipes' +import { createVar, style } from '@vanilla-extract/css' -import { atoms, responsiveStyle, rgb, vars } from '../../css'; +import { atoms, responsiveStyle, rgb, vars } from '../../css' -const radiiVar = createVar(); +const radiiVar = createVar() export const variants = recipe({ variants: { @@ -85,6 +85,6 @@ export const variants = recipe({ ]), }, ], -}); +}) -export type Variants = RecipeVariants; +export type Variants = RecipeVariants diff --git a/packages/components/src/components/Field/Field.docs.mdx b/components/src/components/Field/Field.docs.mdx similarity index 91% rename from packages/components/src/components/Field/Field.docs.mdx rename to components/src/components/Field/Field.docs.mdx index 0d0772d..2e97a82 100644 --- a/packages/components/src/components/Field/Field.docs.mdx +++ b/components/src/components/Field/Field.docs.mdx @@ -4,7 +4,7 @@ description: Wrapper for form content --- ```tsx -import { Field } from '@disco3/components'; +import { Field } from 'degen' ``` ```tsx live=true expand=true @@ -51,8 +51,7 @@ import { Field } from '@disco3/components'; ## Consume ID -The content wrapped by field sets an `id` automatically for accessibility. You -can customize the behavior by using the render prop. +The content wrapped by field sets an `id` automatically for accessibility. You can customize the behavior by using the render prop. ```tsx live=true diff --git a/packages/components/src/components/Field/Field.snippets.tsx b/components/src/components/Field/Field.snippets.tsx similarity index 55% rename from packages/components/src/components/Field/Field.snippets.tsx rename to components/src/components/Field/Field.snippets.tsx index 38d3273..a65696a 100644 --- a/packages/components/src/components/Field/Field.snippets.tsx +++ b/components/src/components/Field/Field.snippets.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; +import * as React from 'react' -import { Snippet } from '!/playroom/src/types'; -import { Field } from './Field'; +import { Snippet } from '!/playroom/src/types' +import { Field } from './Field' export const snippets: Snippet[] = [ { @@ -12,4 +12,4 @@ export const snippets: Snippet[] = [ ), }, -]; +] diff --git a/components/src/components/Field/Field.test.tsx b/components/src/components/Field/Field.test.tsx new file mode 100644 index 0000000..c4b57a0 --- /dev/null +++ b/components/src/components/Field/Field.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Field } from './Field' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render( + +
+ , + ) + expect(screen.getByText(/foo/i)).toBeInTheDocument() + }) +}) diff --git a/components/src/components/Field/Field.tsx b/components/src/components/Field/Field.tsx new file mode 100644 index 0000000..a8dfc0f --- /dev/null +++ b/components/src/components/Field/Field.tsx @@ -0,0 +1,114 @@ +import * as React from 'react' + +import { ReactNodeNoStrings } from '../../types' +import { useFieldIds } from '../../hooks' +import { Context as FieldSetContext } from '../FieldSet' +import { Box, BoxProps } from '../Box' +import { VisuallyHidden } from '../VisuallyHidden' + +type State = ReturnType | undefined +const Context = React.createContext(undefined) + +type NativeFormProps = React.AllHTMLAttributes + +export type FieldBaseProps = { + description?: React.ReactNode + error?: React.ReactNode + hideLabel?: boolean + label: React.ReactNode + labelSecondary?: React.ReactNode + required?: NativeFormProps['required'] + width?: BoxProps['width'] +} + +type Props = FieldBaseProps & { + children: React.ReactElement | ((context: State) => ReactNodeNoStrings) + id?: NativeFormProps['id'] +} + +export const Field = ({ + children, + description, + error, + hideLabel, + id, + label, + labelSecondary, + required, + width = 'full', +}: Props) => { + const fieldSetProps = React.useContext(FieldSetContext) + const ids = useFieldIds({ + id, + description: description !== undefined, + error: error !== undefined, + }) + + const labelContent = ( + + + {label} {required && (required)} + + {labelSecondary && labelSecondary} + + ) + + // Allow children to consume ids or try to clone ids onto it + let content: React.ReactNode | null + if (typeof children === 'function') + content = ( + + {(context) => children(context)} + + ) + else if (children) content = React.cloneElement(children, ids.content) + else content = children + + return ( + + {hideLabel ? ( + {labelContent} + ) : ( + labelContent + )} + + {content} + + {description && ( + + {description} + + )} + + {error && ( + + {error} + + )} + + ) +} diff --git a/components/src/components/Field/index.ts b/components/src/components/Field/index.ts new file mode 100644 index 0000000..0ca865d --- /dev/null +++ b/components/src/components/Field/index.ts @@ -0,0 +1,2 @@ +export { Field } from './Field' +export type { FieldBaseProps } from './Field' diff --git a/components/src/components/FieldSet/FieldSet.docs.mdx b/components/src/components/FieldSet/FieldSet.docs.mdx new file mode 100644 index 0000000..c2f5c8e --- /dev/null +++ b/components/src/components/FieldSet/FieldSet.docs.mdx @@ -0,0 +1,102 @@ +--- +title: FieldSet +description: Group related form elements +--- + +```tsx +import { FieldSet } from 'degen' +``` + +```tsx live=true expand=true +
+ + +
+``` + +## Props + + + +## Description + +```tsx live=true +
+ + +
+``` + +## Status + +```tsx live=true +
+ + +
+``` + +## Disabled + +Uses native [`fieldset[disabled]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset#attr-disabled) to automatically disable form elements inside. + +```tsx live=true +
+ + +
+``` + +## Size + +Notice how the field styles also change inside the `FieldSet` depending on the `size`. + +```tsx live=true + +
+ +
+ +
+ +
+
+``` diff --git a/packages/components/src/components/FieldSet/FieldSet.snippets.tsx b/components/src/components/FieldSet/FieldSet.snippets.tsx similarity index 55% rename from packages/components/src/components/FieldSet/FieldSet.snippets.tsx rename to components/src/components/FieldSet/FieldSet.snippets.tsx index 6ce74cb..c5cb381 100644 --- a/packages/components/src/components/FieldSet/FieldSet.snippets.tsx +++ b/components/src/components/FieldSet/FieldSet.snippets.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; +import * as React from 'react' -import { Snippet } from '!/playroom/src/types'; -import { FieldSet } from './FieldSet'; +import { Snippet } from '!/playroom/src/types' +import { FieldSet } from './FieldSet' export const snippets: Snippet[] = [ { @@ -12,4 +12,4 @@ export const snippets: Snippet[] = [ ), }, -]; +] diff --git a/components/src/components/FieldSet/FieldSet.test.tsx b/components/src/components/FieldSet/FieldSet.test.tsx new file mode 100644 index 0000000..6fd7799 --- /dev/null +++ b/components/src/components/FieldSet/FieldSet.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { FieldSet } from './FieldSet' + +describe('
', () => { + afterEach(cleanup) + + it('renders', () => { + render( +
+
+
, + ) + expect(screen.getByText(/token/i)).toBeInTheDocument() + }) +}) diff --git a/components/src/components/FieldSet/FieldSet.tsx b/components/src/components/FieldSet/FieldSet.tsx new file mode 100644 index 0000000..a9621ab --- /dev/null +++ b/components/src/components/FieldSet/FieldSet.tsx @@ -0,0 +1,119 @@ +import * as React from 'react' + +import { ReactNodeNoStrings } from '../../types' +import { Box, BoxProps } from '../Box' +import { Heading } from '../Heading' +import { Stack } from '../Stack' +import { Tag, TagProps } from '../Tag' +import { Text } from '../Text' + +type NativeFieldSetProps = React.AllHTMLAttributes + +type Props = { + children: ReactNodeNoStrings + description?: string | React.ReactNode + disabled?: NativeFieldSetProps['disabled'] + form?: NativeFieldSetProps['form'] + name?: NativeFieldSetProps['name'] + legend: string + size?: 'medium' | 'small' + space?: BoxProps['gap'] + status?: + | 'required' + | 'optional' + | 'pending' + | 'complete' + | { + name: string + tone: TagProps['tone'] + } +} + +export const Context = React.createContext<{ size?: Props['size'] }>({}) + +export const FieldSet = ({ + children, + description, + disabled, + form, + legend, + name, + size, + space = '4', + status, +}: Props) => { + let statusText: string | undefined + let statusTone: TagProps['tone'] + switch (status) { + case 'complete': { + statusText = 'Complete' + statusTone = 'green' + break + } + case 'required': + case 'pending': { + statusText = status === 'pending' ? 'Pending' : 'Required' + statusTone = 'accent' + break + } + case 'optional': { + statusText = 'Optional' + statusTone = 'secondary' + break + } + } + if (typeof status === 'object') { + statusText = status.name + statusTone = status.tone + } + + const isSmall = size === 'small' + + return ( + + + + {isSmall ? ( + + {legend} + + ) : ( + + {legend} + + )} + {statusTone && statusText && ( + + {statusText} + + )} + + + + {description} + + + + + {children} + + + ) +} diff --git a/components/src/components/FieldSet/index.ts b/components/src/components/FieldSet/index.ts new file mode 100644 index 0000000..cd764f7 --- /dev/null +++ b/components/src/components/FieldSet/index.ts @@ -0,0 +1 @@ +export { FieldSet, Context } from './FieldSet' diff --git a/packages/components/src/components/FileInput/FileInput.docs.mdx b/components/src/components/FileInput/FileInput.docs.mdx similarity index 96% rename from packages/components/src/components/FileInput/FileInput.docs.mdx rename to components/src/components/FileInput/FileInput.docs.mdx index 37045a6..9639864 100644 --- a/packages/components/src/components/FileInput/FileInput.docs.mdx +++ b/components/src/components/FileInput/FileInput.docs.mdx @@ -4,7 +4,7 @@ description: Surface for accepting files --- ```tsx -import { FileInput } from '@disco3/components'; +import { FileInput } from 'degen' ``` ```tsx live=true expand=true diff --git a/packages/components/src/components/FileInput/FileInput.snippets.tsx b/components/src/components/FileInput/FileInput.snippets.tsx similarity index 61% rename from packages/components/src/components/FileInput/FileInput.snippets.tsx rename to components/src/components/FileInput/FileInput.snippets.tsx index e3ccdd3..eafd3f0 100644 --- a/packages/components/src/components/FileInput/FileInput.snippets.tsx +++ b/components/src/components/FileInput/FileInput.snippets.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; +import * as React from 'react' -import { Snippet } from '!/playroom/src/types'; -import { FileInput } from './FileInput'; +import { Snippet } from '!/playroom/src/types' +import { FileInput } from './FileInput' export const snippets: Snippet[] = [ { @@ -12,4 +12,4 @@ export const snippets: Snippet[] = [ ), }, -]; +] diff --git a/components/src/components/FileInput/FileInput.test.tsx b/components/src/components/FileInput/FileInput.test.tsx new file mode 100644 index 0000000..5946454 --- /dev/null +++ b/components/src/components/FileInput/FileInput.test.tsx @@ -0,0 +1,20 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { FileInput } from './FileInput' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render( + + {(context) => + context.name ?
{context.name}
:
Upload file
+ } +
, + ) + expect(screen.getByText(/upload/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/FileInput/FileInput.tsx b/components/src/components/FileInput/FileInput.tsx similarity index 56% rename from packages/components/src/components/FileInput/FileInput.tsx rename to components/src/components/FileInput/FileInput.tsx index 76f95be..6aa697b 100644 --- a/packages/components/src/components/FileInput/FileInput.tsx +++ b/components/src/components/FileInput/FileInput.tsx @@ -1,46 +1,46 @@ -import * as React from 'react'; +import * as React from 'react' -import { ReactNodeNoStrings } from '../../types'; -import { useFieldIds } from '../../hooks'; -import { Box } from '../Box'; -import { VisuallyHidden } from '../VisuallyHidden'; -import { validateAccept } from './utils'; +import { ReactNodeNoStrings } from '../../types' +import { useFieldIds } from '../../hooks' +import { Box } from '../Box' +import { VisuallyHidden } from '../VisuallyHidden' +import { validateAccept } from './utils' type State = { - droppable?: boolean; - file?: File; - focused?: boolean; - name?: string; - previewUrl?: string; - type?: string; -}; + droppable?: boolean + file?: File + focused?: boolean + name?: string + previewUrl?: string + type?: string +} -const initialState: State = {}; +const initialState: State = {} -type NativeInputProps = React.AllHTMLAttributes; +type NativeInputProps = React.AllHTMLAttributes export type Props = { - accept?: NativeInputProps['accept']; - autoFocus?: NativeInputProps['autoFocus']; + accept?: NativeInputProps['accept'] + autoFocus?: NativeInputProps['autoFocus'] children: ( context: State & { - reset(event: React.MouseEvent): void; + reset(event: React.MouseEvent): void }, - ) => ReactNodeNoStrings; - defaultValue?: { name?: string; type: string; url: string }; - disabled?: NativeInputProps['disabled']; - error?: boolean | React.ReactNode; - id?: NativeInputProps['id']; + ) => ReactNodeNoStrings + defaultValue?: { name?: string; type: string; url: string } + disabled?: NativeInputProps['disabled'] + error?: boolean | React.ReactNode + id?: NativeInputProps['id'] /** Size in megabytes */ - maxSize?: number; - name?: string; - required?: NativeInputProps['required']; - tabIndex?: NativeInputProps['tabIndex']; - onBlur?: NativeInputProps['onBlur']; - onError?(error: string): void; - onChange?(file: File): void; - onFocus?: NativeInputProps['onFocus']; - onReset?(): void; -}; + maxSize?: number + name?: string + required?: NativeInputProps['required'] + tabIndex?: NativeInputProps['tabIndex'] + onBlur?: NativeInputProps['onBlur'] + onError?(error: string): void + onChange?(file: File): void + onFocus?: NativeInputProps['onFocus'] + onReset?(): void +} export const FileInput = React.forwardRef( ( @@ -64,134 +64,134 @@ export const FileInput = React.forwardRef( }: Props, ref: React.Ref, ) => { - const defaultRef = React.useRef(null); - const inputRef = (ref as React.RefObject) || defaultRef; - const [state, setState] = React.useState(initialState); + const defaultRef = React.useRef(null) + const inputRef = (ref as React.RefObject) || defaultRef + const [state, setState] = React.useState(initialState) - const hasError = error ? true : undefined; + const hasError = error ? true : undefined const ids = useFieldIds({ id, error: hasError, - }); + }) const handleFile = React.useCallback( (file: File, event?: React.ChangeEvent | React.DragEvent) => { // Disallow file larger than max if (maxSize && file.size > maxSize * 1_000_000) { - event?.preventDefault(); + event?.preventDefault() onError && onError( `File is ${(file.size / 1_000_000).toFixed( 2, )} MB. Must be smaller than ${maxSize} MB`, - ); - return; + ) + return } setState((x) => ({ ...x, file, name: file.name, type: file.type, - })); - onChange && onChange(file); + })) + onChange && onChange(file) }, [maxSize, onChange, onError], - ); + ) const handleChange = React.useCallback( (event: React.ChangeEvent) => { - const files = event.target.files; - if (!files?.length) return; - handleFile(files[0], event); + const files = event.target.files + if (!files?.length) return + handleFile(files[0], event) }, [handleFile], - ); + ) const handleDragOver = React.useCallback( (event: React.DragEvent) => { - event.preventDefault(); - setState((x) => ({ ...x, droppable: true })); + event.preventDefault() + setState((x) => ({ ...x, droppable: true })) }, [], - ); + ) const handleDragLeave = React.useCallback( (event: React.DragEvent) => { - event.preventDefault(); - setState((x) => ({ ...x, droppable: false })); + event.preventDefault() + setState((x) => ({ ...x, droppable: false })) }, [], - ); + ) const handleDrop = React.useCallback( (event: React.DragEvent) => { - event.preventDefault(); - setState((x) => ({ ...x, droppable: false })); - let file: File | null; + event.preventDefault() + setState((x) => ({ ...x, droppable: false })) + let file: File | null if (event.dataTransfer.items) { - const files = event.dataTransfer.items; - if (files?.[0].kind !== 'file') return; - file = files[0].getAsFile(); - if (!file) return; + const files = event.dataTransfer.items + if (files?.[0].kind !== 'file') return + file = files[0].getAsFile() + if (!file) return } else { - const files = event.dataTransfer.files; - if (!files?.length) return; - file = files[0]; + const files = event.dataTransfer.files + if (!files?.length) return + file = files[0] } - if (!validateAccept(file.type, accept)) return; - handleFile(file, event); + if (!validateAccept(file.type, accept)) return + handleFile(file, event) }, [handleFile, accept], - ); + ) const handleFocus = React.useCallback( (event: React.FocusEvent) => { - setState((x) => ({ ...x, focused: true })); - onFocus && onFocus(event); + setState((x) => ({ ...x, focused: true })) + onFocus && onFocus(event) }, [onFocus], - ); + ) const handleBlur = React.useCallback( (event: React.FocusEvent) => { - setState((x) => ({ ...x, focused: false })); - onBlur && onBlur(event); + setState((x) => ({ ...x, focused: false })) + onBlur && onBlur(event) }, [onBlur], - ); + ) /* eslint-disable react-hooks/exhaustive-deps */ const reset = React.useCallback( (event: React.MouseEvent) => { - event.preventDefault(); - setState(initialState); - if (inputRef.current) inputRef.current.value = ''; - onReset && onReset(); + event.preventDefault() + setState(initialState) + if (inputRef.current) inputRef.current.value = '' + onReset && onReset() }, // No need to add defaultValue [inputRef, onReset], - ); + ) /* eslint-enable react-hooks/exhaustive-deps */ // Display preview for default value /* eslint-disable react-hooks/exhaustive-deps */ React.useEffect(() => { - if (!defaultValue) return; + if (!defaultValue) return setState({ previewUrl: defaultValue.url, name: defaultValue.name, type: defaultValue.type, - }); - }, []); + }) + }, []) /* eslint-enable react-hooks/exhaustive-deps */ // Create URL for displaying media preview React.useEffect(() => { - if (!state.file) return; - const previewUrl = URL.createObjectURL(state.file); - setState((x) => ({ ...x, previewUrl })); - return () => URL.revokeObjectURL(previewUrl); - }, [state.file]); + if (!state.file) return + const previewUrl = URL.createObjectURL(state.file) + setState((x) => ({ ...x, previewUrl })) + return () => URL.revokeObjectURL(previewUrl) + }, [state.file]) return ( @@ -224,8 +224,8 @@ export const FileInput = React.forwardRef( {children({ ...state, reset })} - ); + ) }, -); +) -FileInput.displayName = 'FileInput'; +FileInput.displayName = 'FileInput' diff --git a/components/src/components/FileInput/index.ts b/components/src/components/FileInput/index.ts new file mode 100644 index 0000000..e9735a4 --- /dev/null +++ b/components/src/components/FileInput/index.ts @@ -0,0 +1,2 @@ +export { FileInput } from './FileInput' +export type { Props as FileInputProps } from './FileInput' diff --git a/packages/components/src/components/FileInput/utils.test.ts b/components/src/components/FileInput/utils.test.ts similarity index 90% rename from packages/components/src/components/FileInput/utils.test.ts rename to components/src/components/FileInput/utils.test.ts index a0c3fe0..66b471e 100644 --- a/packages/components/src/components/FileInput/utils.test.ts +++ b/components/src/components/FileInput/utils.test.ts @@ -1,4 +1,4 @@ -import { validateAccept } from './utils'; +import { validateAccept } from './utils' describe.each` fileType | accept | expected @@ -11,6 +11,6 @@ describe.each` ${'image/jpeg'} | ${'image/jpeg, image/png, image/webp, image/gif, video/mp4, video/ogg, video/webm'} | ${true} `('validateAccept($fileType, $accept)', ({ fileType, accept, expected }) => { it(`returns ${expected}`, () => { - expect(validateAccept(fileType, accept)).toEqual(expected); - }); -}); + expect(validateAccept(fileType, accept)).toEqual(expected) + }) +}) diff --git a/packages/components/src/components/FileInput/utils.ts b/components/src/components/FileInput/utils.ts similarity index 54% rename from packages/components/src/components/FileInput/utils.ts rename to components/src/components/FileInput/utils.ts index ebfc793..e20f799 100644 --- a/packages/components/src/components/FileInput/utils.ts +++ b/components/src/components/FileInput/utils.ts @@ -1,21 +1,21 @@ -import { Props } from './FileInput'; +import { Props } from './FileInput' export const validateAccept = (fileType: string, accept: Props['accept']) => { - const allowedTypes = accept?.split(', '); - if (!allowedTypes) return true; - const mime = getMimeType(fileType); + const allowedTypes = accept?.split(', ') + if (!allowedTypes) return true + const mime = getMimeType(fileType) return allowedTypes.some((x) => { - const allowedMime = getMimeType(x); + const allowedMime = getMimeType(x) return ( allowedMime.type === mime.type && allowedMime.subtype === mime.subtype - ); - }); -}; + ) + }) +} const getMimeType = (type: string) => { - const parts = type.split('/'); + const parts = type.split('/') return { type: parts[0], subtype: parts[1], - }; -}; + } +} diff --git a/packages/components/src/components/Heading/Heading.docs.mdx b/components/src/components/Heading/Heading.docs.mdx similarity index 81% rename from packages/components/src/components/Heading/Heading.docs.mdx rename to components/src/components/Heading/Heading.docs.mdx index 9458316..a944c2b 100644 --- a/packages/components/src/components/Heading/Heading.docs.mdx +++ b/components/src/components/Heading/Heading.docs.mdx @@ -4,7 +4,7 @@ description: Page and section title --- ```tsx -import { Heading } from '@disco3/components'; +import { Heading } from 'degen' ``` ```tsx expand=true live=true @@ -17,10 +17,7 @@ import { Heading } from '@disco3/components'; ## Levels -By default, the semantic heading level matches the visual heading level, e.g. -`` will render an `h2` element. If you need the semantics to -be different from the visuals, you can provide the desired HTML tag via the `as` -prop. +By default, the semantic heading level matches the visual heading level, e.g. `` will render an `h2` element. If you need the semantics to be different from the visuals, you can provide the desired HTML tag via the `as` prop. ```tsx live=true diff --git a/components/src/components/Heading/Heading.snippets.tsx b/components/src/components/Heading/Heading.snippets.tsx new file mode 100644 index 0000000..15175ef --- /dev/null +++ b/components/src/components/Heading/Heading.snippets.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { Heading } from './Heading' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: Heading, + }, +] diff --git a/components/src/components/Heading/Heading.test.tsx b/components/src/components/Heading/Heading.test.tsx new file mode 100644 index 0000000..e30ec1a --- /dev/null +++ b/components/src/components/Heading/Heading.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Heading } from './Heading' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render(foo bar baz) + expect(screen.getByText(/foo/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/Heading/Heading.tsx b/components/src/components/Heading/Heading.tsx similarity index 59% rename from packages/components/src/components/Heading/Heading.tsx rename to components/src/components/Heading/Heading.tsx index 3a9848b..29618ef 100644 --- a/packages/components/src/components/Heading/Heading.tsx +++ b/components/src/components/Heading/Heading.tsx @@ -1,22 +1,23 @@ -import * as React from 'react'; +import * as React from 'react' -import { Box, BoxProps } from '../Box'; -import * as styles from './styles.css'; +import { Box, BoxProps } from '../Box' +import * as styles from './styles.css' const resolveDefaultComponent = { '1': 'h1', '2': 'h2', -} as const; +} as const type Props = { - align?: BoxProps['textAlign']; - as?: 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'legend'; - children?: React.ReactNode; - color?: BoxProps['color']; - id?: string; - transform?: BoxProps['textTransform']; - responsive?: boolean; -} & styles.Variants; + align?: BoxProps['textAlign'] + as?: 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'legend' + children?: React.ReactNode + color?: BoxProps['color'] + id?: string + transform?: BoxProps['textTransform'] + responsive?: boolean + wordBreak?: BoxProps['wordBreak'] +} & styles.Variants export const Heading = ({ align, @@ -27,6 +28,7 @@ export const Heading = ({ level = '2', responsive, transform, + wordBreak, }: Props) => { return ( {children} - ); -}; + ) +} diff --git a/components/src/components/Heading/index.ts b/components/src/components/Heading/index.ts new file mode 100644 index 0000000..d273982 --- /dev/null +++ b/components/src/components/Heading/index.ts @@ -0,0 +1 @@ +export { Heading } from './Heading' diff --git a/packages/components/src/components/Heading/styles.css.ts b/components/src/components/Heading/styles.css.ts similarity index 86% rename from packages/components/src/components/Heading/styles.css.ts rename to components/src/components/Heading/styles.css.ts index 78669a9..4634436 100644 --- a/packages/components/src/components/Heading/styles.css.ts +++ b/components/src/components/Heading/styles.css.ts @@ -1,7 +1,7 @@ -import { style } from '@vanilla-extract/css'; -import { RecipeVariants, recipe } from '@vanilla-extract/recipes'; +import { style } from '@vanilla-extract/css' +import { RecipeVariants, recipe } from '@vanilla-extract/recipes' -import { atoms } from '../../css'; +import { atoms } from '../../css' const level = { '1': style([ @@ -22,7 +22,7 @@ const level = { lineHeight: '1.25', }), ]), -}; +} export const variants = recipe({ variants: { @@ -53,6 +53,6 @@ export const variants = recipe({ }), }, ], -}); +}) -export type Variants = RecipeVariants; +export type Variants = RecipeVariants diff --git a/packages/components/src/components/Input/Input.docs.mdx b/components/src/components/Input/Input.docs.mdx similarity index 94% rename from packages/components/src/components/Input/Input.docs.mdx rename to components/src/components/Input/Input.docs.mdx index 00d9843..f5ee30e 100644 --- a/packages/components/src/components/Input/Input.docs.mdx +++ b/components/src/components/Input/Input.docs.mdx @@ -4,7 +4,7 @@ description: Surface for accepting user input --- ```tsx -import { Input } from '@disco3/components'; +import { Input } from 'degen' ``` ```tsx live=true expand=true @@ -54,7 +54,7 @@ import { Input } from '@disco3/components'; diff --git a/components/src/components/Input/Input.snippets.tsx b/components/src/components/Input/Input.snippets.tsx new file mode 100644 index 0000000..115065e --- /dev/null +++ b/components/src/components/Input/Input.snippets.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { Input } from './Input' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: , + }, +] diff --git a/components/src/components/Input/Input.test.tsx b/components/src/components/Input/Input.test.tsx new file mode 100644 index 0000000..8005762 --- /dev/null +++ b/components/src/components/Input/Input.test.tsx @@ -0,0 +1,67 @@ +import * as React from 'react' + +import { cleanup, render, screen, userEvent } from '@/test' + +import { Input } from './Input' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render() + expect(screen.getByRole('textbox')).toBeInTheDocument() + }) + + it('receives user input', () => { + render() + + userEvent.type(screen.getByRole('textbox'), 'Satoshi Nakamoto') + expect(screen.getByRole('textbox')).toHaveValue('Satoshi Nakamoto') + }) + + describe('[type=text]', () => { + it('maxLength', () => { + render() + + const element = screen.getByLabelText(/short/i) + userEvent.type(element, 'Satoshi Nakamoto') + expect(element).toHaveValue('Satoshi') + }) + }) + + describe('[type=number]', () => { + it('filters invalid characters', () => { + render() + + const element = screen.getByLabelText(/funding/i) + userEvent.type(element, 'Ee+') + expect(element).toHaveValue(null) + }) + + it('max', () => { + render() + + const element = screen.getByLabelText(/funding/i) + expect(element).toHaveValue(null) + userEvent.click(screen.getByText('Max')) + expect(element).toHaveValue(20) + }) + + it('units', () => { + render( + , + ) + + const element = screen.getByLabelText(/funding/i) as HTMLInputElement + expect(element.placeholder).toEqual('10 ETH') + userEvent.type(element, '20') + expect(element).toHaveValue(20) + expect(screen.getByTestId('ghost')).toBeInTheDocument() + }) + }) +}) diff --git a/packages/components/src/components/Input/Input.tsx b/components/src/components/Input/Input.tsx similarity index 72% rename from packages/components/src/components/Input/Input.tsx rename to components/src/components/Input/Input.tsx index 364f6e6..98ad083 100644 --- a/packages/components/src/components/Input/Input.tsx +++ b/components/src/components/Input/Input.tsx @@ -1,52 +1,52 @@ -import * as React from 'react'; +import * as React from 'react' -import { Box, BoxProps } from '../Box'; -import { Field, FieldBaseProps } from '../Field'; -import * as styles from './styles.css'; +import { Box, BoxProps } from '../Box' +import { Field, FieldBaseProps } from '../Field' +import * as styles from './styles.css' -type NativeInputProps = React.AllHTMLAttributes; +type NativeInputProps = React.AllHTMLAttributes type BaseProps = FieldBaseProps & { - autoFocus?: NativeInputProps['autoFocus']; - autoComplete?: NativeInputProps['autoComplete']; - autoCorrect?: NativeInputProps['autoCorrect']; - defaultValue?: string | number; - disabled?: boolean; - id?: NativeInputProps['id']; - inputMode?: NativeInputProps['inputMode']; - name?: string; - placeholder?: NativeInputProps['placeholder']; - prefix?: React.ReactNode; - readOnly?: NativeInputProps['readOnly']; - spellCheck?: NativeInputProps['spellCheck']; - suffix?: React.ReactNode; - tabIndex?: NativeInputProps['tabIndex']; - textTransform?: BoxProps['textTransform']; - type?: 'email' | 'number' | 'text'; - units?: string; - value?: string | number; - onBlur?: NativeInputProps['onBlur']; - onChange?: React.EventHandler>; - onFocus?: NativeInputProps['onFocus']; - onKeyDown?: React.KeyboardEventHandler; -}; + autoFocus?: NativeInputProps['autoFocus'] + autoComplete?: NativeInputProps['autoComplete'] + autoCorrect?: NativeInputProps['autoCorrect'] + defaultValue?: string | number + disabled?: boolean + id?: NativeInputProps['id'] + inputMode?: NativeInputProps['inputMode'] + name?: string + placeholder?: NativeInputProps['placeholder'] + prefix?: React.ReactNode + readOnly?: NativeInputProps['readOnly'] + spellCheck?: NativeInputProps['spellCheck'] + suffix?: React.ReactNode + tabIndex?: NativeInputProps['tabIndex'] + textTransform?: BoxProps['textTransform'] + type?: 'email' | 'number' | 'text' + units?: string + value?: string | number + onBlur?: NativeInputProps['onBlur'] + onChange?: React.EventHandler> + onFocus?: NativeInputProps['onFocus'] + onKeyDown?: React.KeyboardEventHandler +} type WithTypeEmail = { - type?: 'email'; -}; + type?: 'email' +} type WithTypeText = { - type?: 'text'; - maxLength?: NativeInputProps['maxLength']; -}; + type?: 'text' + maxLength?: NativeInputProps['maxLength'] +} type WithTypeNumber = { - type?: 'number'; - max?: NativeInputProps['max']; - min?: NativeInputProps['min']; -}; + type?: 'number' + max?: NativeInputProps['max'] + min?: NativeInputProps['min'] +} -type Props = BaseProps & (WithTypeEmail | WithTypeText | WithTypeNumber); +type Props = BaseProps & (WithTypeEmail | WithTypeText | WithTypeNumber) export const Input = React.forwardRef( ( @@ -84,60 +84,60 @@ export const Input = React.forwardRef( }: Props, ref: React.Ref, ) => { - const defaultRef = React.useRef(null); - const inputRef = (ref as React.RefObject) || defaultRef; + const defaultRef = React.useRef(null) + const inputRef = (ref as React.RefObject) || defaultRef const [state, setState] = React.useState<{ - ghostValue?: Props['value']; - }>({ ghostValue: value || defaultValue }); + ghostValue?: Props['value'] + }>({ ghostValue: value || defaultValue }) const placeholderText = placeholder ? `${placeholder ?? ''}${units ? ` ${units}` : ''}` - : undefined; - const hasError = error ? true : undefined; + : undefined + const hasError = error ? true : undefined const className = styles.variants({ prefix: prefix ? true : undefined, suffix: suffix ? true : undefined, - }); - const max = (props as WithTypeNumber).max; - const inputType = type === 'number' ? 'number' : 'text'; + }) + const max = (props as WithTypeNumber).max + const inputType = type === 'number' ? 'number' : 'text' const handleInput = React.useCallback( (event: React.FormEvent) => { - const value = (event.target as HTMLInputElement).value; - setState((x) => ({ ...x, ghostValue: value })); + const value = (event.target as HTMLInputElement).value + setState((x) => ({ ...x, ghostValue: value })) }, [], - ); + ) const handleKeyDown = React.useCallback( (event: React.KeyboardEvent) => { if (type === 'number') { - const key = event.key; - const filteredKeys = ['E', 'e', '+']; - if (filteredKeys.includes(key)) event.preventDefault(); + const key = event.key + const filteredKeys = ['E', 'e', '+'] + if (filteredKeys.includes(key)) event.preventDefault() } - onKeyDown && onKeyDown(event); + onKeyDown && onKeyDown(event) }, [type, onKeyDown], - ); + ) const handleWheel = React.useCallback( (event: React.WheelEvent) => { - (event.target as HTMLElement)?.blur(); + ;(event.target as HTMLElement)?.blur() }, [], - ); + ) const handleMax = React.useCallback(() => { if (onChange) onChange({ target: { value: max }, - } as React.ChangeEvent); - else if (inputRef.current) inputRef.current.value = max as string; - if (!units) return; - setState((x) => ({ ...x, ghostValue: max })); - }, [inputRef, max, units, onChange]); + } as React.ChangeEvent) + else if (inputRef.current) inputRef.current.value = max as string + if (!units) return + setState((x) => ({ ...x, ghostValue: max })) + }, [inputRef, max, units, onChange]) return ( )} - ); + ) }, -); +) -Input.displayName = 'Input'; +Input.displayName = 'Input' diff --git a/components/src/components/Input/index.ts b/components/src/components/Input/index.ts new file mode 100644 index 0000000..3188ccc --- /dev/null +++ b/components/src/components/Input/index.ts @@ -0,0 +1 @@ +export { Input } from './Input' diff --git a/packages/components/src/components/Input/styles.css.ts b/components/src/components/Input/styles.css.ts similarity index 93% rename from packages/components/src/components/Input/styles.css.ts rename to components/src/components/Input/styles.css.ts index 010383e..544b425 100644 --- a/packages/components/src/components/Input/styles.css.ts +++ b/components/src/components/Input/styles.css.ts @@ -1,9 +1,9 @@ -import { style } from '@vanilla-extract/css'; -import { recipe } from '@vanilla-extract/recipes'; +import { style } from '@vanilla-extract/css' +import { recipe } from '@vanilla-extract/recipes' -import { atoms, vars } from '../../css'; +import { atoms, vars } from '../../css' -export const inputParent = style({}); +export const inputParent = style({}) export const root = recipe({ base: [ @@ -50,31 +50,31 @@ export const root = recipe({ ]), }, }, -}); +}) const container = atoms({ alignItems: 'center', display: 'flex', height: 'full', lineHeight: 'none', -}); +}) const text = atoms({ color: 'inherit', fontFamily: 'sans', fontSize: 'base', fontWeight: 'medium', -}); +}) -const affix = style([container, text, style({ lineHeight: 'normal' })]); +const affix = style([container, text, style({ lineHeight: 'normal' })]) export const prefix = style([ affix, atoms({ paddingLeft: '4', paddingRight: '2' }), -]); +]) export const suffix = style([ affix, atoms({ paddingRight: '4', paddingLeft: '2' }), -]); +]) export const input = recipe({ base: atoms({ @@ -97,7 +97,7 @@ export const input = recipe({ text: {}, }, }, -}); +}) export const ghost = recipe({ base: [ @@ -119,7 +119,7 @@ export const ghost = recipe({ text: {}, }, }, -}); +}) export const variants = recipe({ base: [style([container, text, atoms({ paddingX: '4' })])], @@ -135,7 +135,7 @@ export const variants = recipe({ }), }, }, -}); +}) export const max = style([ atoms({ @@ -164,4 +164,4 @@ export const max = style([ }, }, }), -]); +]) diff --git a/components/src/components/MediaPicker/MediaPicker.docs.mdx b/components/src/components/MediaPicker/MediaPicker.docs.mdx new file mode 100644 index 0000000..d7db92a --- /dev/null +++ b/components/src/components/MediaPicker/MediaPicker.docs.mdx @@ -0,0 +1,80 @@ +--- +title: MediaPicker +description: Surface for accepting media +--- + +```tsx +import { MediaPicker } from 'degen' +``` + +```tsx live=true expand=true + +``` + +## Props + + + +## Disabled + +```tsx live=true + +``` + +## Error + +```tsx live=true + +``` + +## Uploading + +```tsx live=true + +``` + +## Uploaded + +```tsx live=true + +``` + +## Compact + +```tsx live=true + +``` + +## Cover + +```tsx live=true + +``` + +## Content Types + +```tsx live=true + + + + +``` diff --git a/components/src/components/MediaPicker/MediaPicker.snippets.tsx b/components/src/components/MediaPicker/MediaPicker.snippets.tsx new file mode 100644 index 0000000..ec07812 --- /dev/null +++ b/components/src/components/MediaPicker/MediaPicker.snippets.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { MediaPicker } from './MediaPicker' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: , + }, +] diff --git a/components/src/components/MediaPicker/MediaPicker.test.tsx b/components/src/components/MediaPicker/MediaPicker.test.tsx new file mode 100644 index 0000000..c5968db --- /dev/null +++ b/components/src/components/MediaPicker/MediaPicker.test.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' + +import { cleanup, render, screen, userEvent } from '@/test' + +import { MediaPicker } from './MediaPicker' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render() + expect(screen.getByLabelText(/choose/i)).toBeInTheDocument() + }) + + it('receives user input', () => { + render() + + const file = new File(['hello'], 'hello.png', { type: 'image/png' }) + const input = screen.getByLabelText(/choose/i) as HTMLInputElement + userEvent.upload(input, file) + + expect(input?.files?.[0]).toStrictEqual(file) + expect(input?.files?.item(0)).toStrictEqual(file) + expect(input.files).toHaveLength(1) + }) +}) diff --git a/components/src/components/MediaPicker/MediaPicker.tsx b/components/src/components/MediaPicker/MediaPicker.tsx new file mode 100644 index 0000000..23c3075 --- /dev/null +++ b/components/src/components/MediaPicker/MediaPicker.tsx @@ -0,0 +1,306 @@ +import * as React from 'react' + +import { Box, BoxProps } from '../Box' +import { Button } from '../Button' +import { FileInput, FileInputProps } from '../FileInput' +import { Spinner } from '../Spinner' +import { Tag } from '../Tag' +import { VisuallyHidden } from '../VisuallyHidden' +import { IconClose, IconExclamation, IconUpload } from '../icons' +import * as styles from './styles.css' + +type Image = + | 'image/jpeg' + | 'image/png' + | 'image/webp' + | 'image/jpeg, image/png, image/webp' +type Video = + | 'image/gif' + | 'video/mp4' + | 'video/ogg' + | 'video/webm' + | 'image/gif, video/mp4, video/ogg, video/webm' +export type Accept = Image | Video | `${Image}, ${Video}` + +type BaseProps = { + accept?: Accept + autoFocus?: FileInputProps['autoFocus'] + defaultValue?: { name?: string; type: string; url: string } + disabled?: FileInputProps['disabled'] + error?: boolean | React.ReactNode + id?: FileInputProps['id'] + label: React.ReactNode + /** Size in megabytes */ + maxSize?: number + name?: string + required?: FileInputProps['required'] + tabIndex?: FileInputProps['tabIndex'] + uploaded?: boolean + uploading?: boolean + uploadProgress?: number + onBlur?: FileInputProps['onBlur'] + onError?(error: string): void + onChange?: FileInputProps['onChange'] + onFocus?: FileInputProps['onFocus'] + onReset?: FileInputProps['onReset'] +} + +type WithoutCompact = { + compact?: never + cover?: boolean +} + +type WithCompact = { + /** Show smaller input */ + compact?: boolean + cover?: never +} + +type Props = BaseProps & (WithCompact | WithoutCompact) + +export const MediaPicker = ({ + accept = 'image/jpeg, image/png, image/webp, image/gif, video/mp4, video/ogg, video/webm', + autoFocus, + compact, + cover, + defaultValue, + disabled, + error, + id, + label, + maxSize = 5, + name, + required, + tabIndex, + uploaded, + uploading, + uploadProgress, + onBlur, + onChange, + onError, + onFocus, + onReset, +}: Props) => { + const hasError = error ? true : undefined + return ( + + {(context) => ( + + + + + + + {!cover && context.file ? ( + context.file.name + ) : ( + <> + {label}{' '} + {required && ( + (required) + )} + + )} + + + + + + {cover && context.type && context.previewUrl && ( + + + + )} + + + {context.type && ( + + + + )} + + )} + + ) +} + +MediaPicker.displayName = 'MediaPicker' + +type MediaProps = { + cover?: boolean + name?: string + type: string + url: string +} + +const Media = ({ cover, name, type, url }: MediaProps) => { + const boxProps: BoxProps = cover + ? { className: styles.cover } + : { + maxHeight: 'full', + maxWidth: 'full', + } + if (type.includes('image')) + return + else if (type.includes('video')) + return + return null +} + +type MediaPreviewProps = Pick & { + fileName?: string + fileType?: string + hasError?: boolean + previewUrl?: string +} + +const MediaPreview = ({ + compact, + fileName, + fileType, + hasError, + previewUrl, + uploading, +}: MediaPreviewProps) => { + let content: React.ReactNode + if (uploading) content = + else if (fileType && previewUrl) + content = + else if (hasError) content = + else content = + + return ( + + {content} + + ) +} + +type MediaTagProps = Pick< + Props, + 'compact' | 'error' | 'maxSize' | 'uploadProgress' | 'uploaded' | 'uploading' +> + +const MediaTag = ({ + compact, + error, + maxSize, + uploadProgress, + uploaded, + uploading, +}: MediaTagProps) => { + let statusProps: Parameters[0] | undefined + if (uploading) + statusProps = { + tone: 'accent', + ...(uploadProgress + ? { label: 'Uploading', children: `${uploadProgress * 100}%` } + : { children: 'Uploading' }), + } + else if (uploaded) statusProps = { children: 'Success', tone: 'green' } + else if (error) + statusProps = { + tone: 'red', + ...(typeof error === 'string' + ? { label: 'Error', children: error } + : { children: 'Error' }), + } + else if (maxSize !== undefined) + statusProps = { + label: 'Maximum size', + children: `${maxSize} MB`, + } + + if (!statusProps) return null + return +} + +type RemoveButtonProps = Pick & { + onClick(event: React.MouseEvent): void +} + +const RemoveButton = ({ cover, uploading, onClick }: RemoveButtonProps) => { + const content = ( + <> + Remove Media + + + ) + if (cover) + return ( + + {uploading ? : content} + + ) + + return ( + + ) +} diff --git a/components/src/components/MediaPicker/index.ts b/components/src/components/MediaPicker/index.ts new file mode 100644 index 0000000..905799b --- /dev/null +++ b/components/src/components/MediaPicker/index.ts @@ -0,0 +1 @@ +export { MediaPicker } from './MediaPicker' diff --git a/components/src/components/MediaPicker/styles.css.ts b/components/src/components/MediaPicker/styles.css.ts new file mode 100644 index 0000000..01fb327 --- /dev/null +++ b/components/src/components/MediaPicker/styles.css.ts @@ -0,0 +1,148 @@ +import { style } from '@vanilla-extract/css' +import { recipe } from '@vanilla-extract/recipes' + +import { atoms, rgb, vars } from '../../css' + +export const root = recipe({ + base: [ + atoms({ + borderRadius: '2xLarge', + borderWidth: '0.5', + overflow: 'hidden', + position: 'relative', + transitionDuration: '150', + transitionProperty: 'colors', + transitionTimingFunction: 'inOut', + width: 'full', + }), + ], + variants: { + disabled: { + true: atoms({ + borderColor: 'foregroundSecondary', + cursor: 'not-allowed', + }), + }, + droppable: { + true: style({ + borderStyle: 'dashed', + }), + }, + focused: { + true: atoms({ + borderColor: 'accent', + }), + false: atoms({ + borderColor: 'foregroundSecondary', + }), + }, + }, +}) + +export const label = recipe({ + base: atoms({ + alignItems: 'center', + display: 'flex', + cursor: 'pointer', + flexDirection: 'column', + gap: '5', + width: 'full', + }), + variants: { + compact: { + true: atoms({ + flexDirection: 'row', + padding: '4', + paddingRight: '12', + }), + false: atoms({ + padding: '10', + }), + }, + disabled: { + true: atoms({ + cursor: 'not-allowed', + }), + }, + }, + defaultVariants: { + compact: false, + }, +}) + +export const preview = recipe({ + base: atoms({ + alignItems: 'center', + backgroundColor: 'foregroundTertiary', + borderRadius: 'large', + color: 'textTertiary', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + overflow: 'hidden', + position: 'relative', + }), + variants: { + compact: { + true: atoms({ + height: '16', + minWidth: '16', + }), + false: atoms({ + height: '32', + width: '32', + }), + }, + }, + defaultVariants: { + compact: false, + }, +}) + +export const content = recipe({ + base: atoms({ + display: 'flex', + flexDirection: 'column', + }), + variants: { + compact: { + true: atoms({ + alignItems: 'flex-start', + gap: '1.5', + textAlign: 'left', + }), + false: atoms({ + alignItems: 'center', + gap: '4', + textAlign: 'center', + }), + }, + }, + defaultVariants: { + compact: false, + }, +}) + +export const cover = style([ + atoms({ + backgroundColor: 'background', + minHeight: 'full', + minWidth: 'full', + }), + style({ + objectFit: 'cover', + objectPosition: 'center', + }), +]) + +export const removeButton = style({ + background: `linear-gradient(0deg, ${rgb( + vars.mode.colors.background, + '0.8', + )}, ${rgb(vars.mode.colors.background, '0.8')}), ${rgb( + vars.mode.colors.foreground, + '0.075', + )}`, + backdropFilter: 'blur(4px)', + color: rgb(vars.mode.colors.foreground, '0.4'), +}) diff --git a/components/src/components/Range/Range.docs.mdx b/components/src/components/Range/Range.docs.mdx new file mode 100644 index 0000000..0772bdf --- /dev/null +++ b/components/src/components/Range/Range.docs.mdx @@ -0,0 +1,16 @@ +--- +title: Range +description: A native element +--- + +```tsx +import { Range } from 'degen' +``` + +```tsx live=true expand=true + +``` + +## Props + + diff --git a/components/src/components/Range/Range.snippets.tsx b/components/src/components/Range/Range.snippets.tsx new file mode 100644 index 0000000..f7ad458 --- /dev/null +++ b/components/src/components/Range/Range.snippets.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { Range } from './Range' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: , + }, +] diff --git a/components/src/components/Range/Range.test.tsx b/components/src/components/Range/Range.test.tsx new file mode 100644 index 0000000..6949778 --- /dev/null +++ b/components/src/components/Range/Range.test.tsx @@ -0,0 +1,13 @@ +import * as React from 'react' + +import { cleanup, render } from '@/test' + +import { Range } from './Range' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render() + }) +}) diff --git a/components/src/components/Range/Range.tsx b/components/src/components/Range/Range.tsx new file mode 100644 index 0000000..17e77ec --- /dev/null +++ b/components/src/components/Range/Range.tsx @@ -0,0 +1,66 @@ +import * as React from 'react' + +import { Box, BoxProps } from '../Box' +import * as styles from './styles.css' + +type Props = Pick< + BoxProps, + | 'marginX' + | 'marginY' + | 'width' + | 'flex' + | 'flexBasis' + | 'flexGrow' + | 'flexShrink' +> & + React.InputHTMLAttributes + +export const Range = React.forwardRef( + ( + { + width = 'full', + marginX = '0', + marginY = '0', + step, + min, + max, + value, + onChange, + form, + name, + disabled, + flex, + flexBasis, + flexGrow, + flexShrink, + }: Props, + ref: React.Ref, + ) => { + return ( + + ) + }, +) + +Range.displayName = 'Range' diff --git a/components/src/components/Range/index.ts b/components/src/components/Range/index.ts new file mode 100644 index 0000000..e947221 --- /dev/null +++ b/components/src/components/Range/index.ts @@ -0,0 +1 @@ +export { Range } from './Range' diff --git a/components/src/components/Range/styles.css.ts b/components/src/components/Range/styles.css.ts new file mode 100644 index 0000000..2417a72 --- /dev/null +++ b/components/src/components/Range/styles.css.ts @@ -0,0 +1,74 @@ +import { + StyleRule, + assignVars, + createThemeContract, + style, +} from '@vanilla-extract/css' +import { calc } from '@vanilla-extract/css-utils' + +import { vars as globalVars } from '../../css' + +/** + * Tested on Chrome, Safari, Firefox + * All input styling techniques derived from: + * https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/ + */ + +const vars = createThemeContract({ + trackHeight: null, + thumbSize: null, +}) + +const track: StyleRule = { + width: globalVars.space.full, + height: vars.trackHeight, + background: globalVars.colors.foregroundSecondary, + borderRadius: globalVars.radii.full, +} + +const thumb: StyleRule = { + borderRadius: globalVars.radii.full, + height: vars.thumbSize, + width: vars.thumbSize, + background: globalVars.colors.accent, + cursor: 'pointer', +} + +export const range = style({ + vars: assignVars(vars, { + trackHeight: globalVars.space['0.5'], + thumbSize: globalVars.space[6], + }), + + WebkitAppearance: 'none', + appearance: 'none', + height: vars.thumbSize, + background: 'none', + + // Track styles + + '::-webkit-slider-runnable-track': { + ...track, + }, + + '::-moz-range-track': { + ...track, + }, + + // Thumb styles and browser-specific overrides + + '::-webkit-slider-thumb': { + ...thumb, + WebkitAppearance: 'none', + marginTop: calc(vars.trackHeight) + .subtract(vars.thumbSize) + .divide(2) + .toString(), // Centers thumb on webkit + boxShadow: 'none', // iOS + }, + + '::-moz-range-thumb': { + ...thumb, + borderWidth: 0, + }, +}) diff --git a/packages/components/src/components/Skeleton/Skeleton.docs.mdx b/components/src/components/Skeleton/Skeleton.docs.mdx similarity index 82% rename from packages/components/src/components/Skeleton/Skeleton.docs.mdx rename to components/src/components/Skeleton/Skeleton.docs.mdx index 5580053..e384c4d 100644 --- a/packages/components/src/components/Skeleton/Skeleton.docs.mdx +++ b/components/src/components/Skeleton/Skeleton.docs.mdx @@ -4,7 +4,7 @@ description: Loading placeholder --- ```tsx -import { Skeleton } from '@disco3/components'; +import { Skeleton } from 'degen' ``` ```tsx expand=true live=true @@ -17,9 +17,7 @@ import { Skeleton } from '@disco3/components'; ## Wrap content -Can wrap content and the container will automatically resize. Check out -[`SkeletonGroup`](/components/SkeletonGroup) if you want to manage multiple -skeletons with a single `loading` prop. +Can wrap content and the container will automatically resize. Check out [`SkeletonGroup`](/components/SkeletonGroup) if you want to manage multiple skeletons with a single `loading` prop. ```tsx live=true diff --git a/components/src/components/Skeleton/Skeleton.snippets.tsx b/components/src/components/Skeleton/Skeleton.snippets.tsx new file mode 100644 index 0000000..7f67683 --- /dev/null +++ b/components/src/components/Skeleton/Skeleton.snippets.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { Skeleton } from './Skeleton' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: _, + }, +] diff --git a/components/src/components/Skeleton/Skeleton.test.tsx b/components/src/components/Skeleton/Skeleton.test.tsx new file mode 100644 index 0000000..191145d --- /dev/null +++ b/components/src/components/Skeleton/Skeleton.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Skeleton } from './Skeleton' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render(foo bar baz) + expect(screen.getByText(/foo/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/Skeleton/Skeleton.tsx b/components/src/components/Skeleton/Skeleton.tsx similarity index 53% rename from packages/components/src/components/Skeleton/Skeleton.tsx rename to components/src/components/Skeleton/Skeleton.tsx index d46e5e0..df656d5 100644 --- a/packages/components/src/components/Skeleton/Skeleton.tsx +++ b/components/src/components/Skeleton/Skeleton.tsx @@ -1,18 +1,18 @@ -import * as React from 'react'; +import * as React from 'react' -import { Box, BoxProps } from '../Box'; -import { Context } from '../SkeletonGroup'; -import * as styles from './styles.css'; +import { Box, BoxProps } from '../Box' +import { Context } from '../SkeletonGroup' +import * as styles from './styles.css' type Props = { - as?: 'div' | 'span'; - backgroundColor?: BoxProps['backgroundColor']; - radius?: BoxProps['borderRadius']; - height?: BoxProps['height']; - loading?: boolean; - maxWidth?: BoxProps['maxWidth']; - width?: BoxProps['width']; -}; + as?: 'div' | 'span' + backgroundColor?: BoxProps['backgroundColor'] + radius?: BoxProps['borderRadius'] + height?: BoxProps['height'] + loading?: boolean + maxWidth?: BoxProps['maxWidth'] + width?: BoxProps['width'] +} export const Skeleton = ({ as, @@ -24,8 +24,8 @@ export const Skeleton = ({ maxWidth, width = 'fit', }: React.PropsWithChildren) => { - const groupLoading = React.useContext(Context); - const active = loading ?? groupLoading; + const groupLoading = React.useContext(Context) + const active = loading ?? groupLoading const containerProps = active ? { backgroundColor, @@ -34,7 +34,7 @@ export const Skeleton = ({ maxWidth, width, } - : {}; + : {} return ( - ); -}; + ) +} diff --git a/components/src/components/Skeleton/index.ts b/components/src/components/Skeleton/index.ts new file mode 100644 index 0000000..e42e3eb --- /dev/null +++ b/components/src/components/Skeleton/index.ts @@ -0,0 +1 @@ +export { Skeleton } from './Skeleton' diff --git a/packages/components/src/components/Skeleton/styles.css.ts b/components/src/components/Skeleton/styles.css.ts similarity index 51% rename from packages/components/src/components/Skeleton/styles.css.ts rename to components/src/components/Skeleton/styles.css.ts index f4a4f29..0467a07 100644 --- a/packages/components/src/components/Skeleton/styles.css.ts +++ b/components/src/components/Skeleton/styles.css.ts @@ -1,5 +1,5 @@ -import { style } from '@vanilla-extract/css'; +import { style } from '@vanilla-extract/css' export const root = style({ visibility: 'hidden', -}); +}) diff --git a/packages/components/src/components/SkeletonGroup/SkeletonGroup.docs.mdx b/components/src/components/SkeletonGroup/SkeletonGroup.docs.mdx similarity index 92% rename from packages/components/src/components/SkeletonGroup/SkeletonGroup.docs.mdx rename to components/src/components/SkeletonGroup/SkeletonGroup.docs.mdx index 070bf2d..c7f784b 100644 --- a/packages/components/src/components/SkeletonGroup/SkeletonGroup.docs.mdx +++ b/components/src/components/SkeletonGroup/SkeletonGroup.docs.mdx @@ -4,7 +4,7 @@ description: Manages loading state for multiple skeletons --- ```tsx -import { SkeletonGroup } from '@disco3/components'; +import { SkeletonGroup } from 'degen' ``` ```tsx expand=true live=true diff --git a/components/src/components/SkeletonGroup/SkeletonGroup.snippets.tsx b/components/src/components/SkeletonGroup/SkeletonGroup.snippets.tsx new file mode 100644 index 0000000..716c9de --- /dev/null +++ b/components/src/components/SkeletonGroup/SkeletonGroup.snippets.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { Skeleton } from '../Skeleton' +import { SkeletonGroup } from './SkeletonGroup' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: ( + + _ + + ), + }, +] diff --git a/components/src/components/SkeletonGroup/SkeletonGroup.test.tsx b/components/src/components/SkeletonGroup/SkeletonGroup.test.tsx new file mode 100644 index 0000000..d731922 --- /dev/null +++ b/components/src/components/SkeletonGroup/SkeletonGroup.test.tsx @@ -0,0 +1,20 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Skeleton } from '../Skeleton' +import { SkeletonGroup } from './SkeletonGroup' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render( + + foo bar baz + quick brown fox + , + ) + expect(screen.getByText(/foo/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/SkeletonGroup/SkeletonGroup.tsx b/components/src/components/SkeletonGroup/SkeletonGroup.tsx similarity index 56% rename from packages/components/src/components/SkeletonGroup/SkeletonGroup.tsx rename to components/src/components/SkeletonGroup/SkeletonGroup.tsx index d125b20..9de628d 100644 --- a/packages/components/src/components/SkeletonGroup/SkeletonGroup.tsx +++ b/components/src/components/SkeletonGroup/SkeletonGroup.tsx @@ -1,14 +1,14 @@ -import * as React from 'react'; +import * as React from 'react' -import { ReactNodeNoStrings } from '../../types'; +import { ReactNodeNoStrings } from '../../types' type Props = { - children: ReactNodeNoStrings; - loading?: boolean; -}; + children: ReactNodeNoStrings + loading?: boolean +} -export const Context = React.createContext(undefined); +export const Context = React.createContext(undefined) export const SkeletonGroup = ({ children, loading }: Props) => { - return {children}; -}; + return {children} +} diff --git a/components/src/components/SkeletonGroup/index.ts b/components/src/components/SkeletonGroup/index.ts new file mode 100644 index 0000000..a3d1a13 --- /dev/null +++ b/components/src/components/SkeletonGroup/index.ts @@ -0,0 +1 @@ +export { SkeletonGroup, Context } from './SkeletonGroup' diff --git a/packages/components/src/components/Spinner/Spinner.docs.mdx b/components/src/components/Spinner/Spinner.docs.mdx similarity index 91% rename from packages/components/src/components/Spinner/Spinner.docs.mdx rename to components/src/components/Spinner/Spinner.docs.mdx index b3a2aa2..4f557bb 100644 --- a/packages/components/src/components/Spinner/Spinner.docs.mdx +++ b/components/src/components/Spinner/Spinner.docs.mdx @@ -4,7 +4,7 @@ description: Loading spinner --- ```tsx -import { Spinner } from '@disco3/components'; +import { Spinner } from 'degen' ``` ```tsx expand=true live=true diff --git a/components/src/components/Spinner/Spinner.snippets.tsx b/components/src/components/Spinner/Spinner.snippets.tsx new file mode 100644 index 0000000..8b2a9c0 --- /dev/null +++ b/components/src/components/Spinner/Spinner.snippets.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { Spinner } from './Spinner' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: , + }, +] diff --git a/components/src/components/Spinner/Spinner.test.tsx b/components/src/components/Spinner/Spinner.test.tsx new file mode 100644 index 0000000..6e327ab --- /dev/null +++ b/components/src/components/Spinner/Spinner.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Spinner } from './Spinner' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render() + expect(screen.getByText(/loading/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/Spinner/Spinner.tsx b/components/src/components/Spinner/Spinner.tsx similarity index 74% rename from packages/components/src/components/Spinner/Spinner.tsx rename to components/src/components/Spinner/Spinner.tsx index 5a28854..e4fab33 100644 --- a/packages/components/src/components/Spinner/Spinner.tsx +++ b/components/src/components/Spinner/Spinner.tsx @@ -1,13 +1,13 @@ -import * as React from 'react'; +import * as React from 'react' -import { Box, BoxProps } from '../Box'; -import { VisuallyHidden } from '../VisuallyHidden'; -import * as styles from './styles.css'; +import { Box, BoxProps } from '../Box' +import { VisuallyHidden } from '../VisuallyHidden' +import * as styles from './styles.css' type Props = { - accessibilityLabel?: string; - color?: BoxProps['color']; -} & styles.Variants; + accessibilityLabel?: string + color?: BoxProps['color'] +} & styles.Variants export const Spinner = React.forwardRef( ( @@ -38,8 +38,8 @@ export const Spinner = React.forwardRef( /> - ); + ) }, -); +) -Spinner.displayName = 'Spinner'; +Spinner.displayName = 'Spinner' diff --git a/components/src/components/Spinner/index.ts b/components/src/components/Spinner/index.ts new file mode 100644 index 0000000..fbf16c1 --- /dev/null +++ b/components/src/components/Spinner/index.ts @@ -0,0 +1 @@ +export { Spinner } from './Spinner' diff --git a/packages/components/src/components/Spinner/styles.css.ts b/components/src/components/Spinner/styles.css.ts similarity index 76% rename from packages/components/src/components/Spinner/styles.css.ts rename to components/src/components/Spinner/styles.css.ts index 45cba71..7e55239 100644 --- a/packages/components/src/components/Spinner/styles.css.ts +++ b/components/src/components/Spinner/styles.css.ts @@ -1,11 +1,11 @@ -import { RecipeVariants, recipe } from '@vanilla-extract/recipes'; -import { keyframes, style } from '@vanilla-extract/css'; +import { RecipeVariants, recipe } from '@vanilla-extract/recipes' +import { keyframes, style } from '@vanilla-extract/css' -import { atoms, motionSafe, vars } from '../../css'; +import { atoms, motionSafe, vars } from '../../css' const rotate = keyframes({ '100%': { transform: 'rotate(1turn)' }, -}); +}) export const variants = recipe({ base: [ @@ -30,6 +30,6 @@ export const variants = recipe({ }), }, }, -}); +}) -export type Variants = RecipeVariants; +export type Variants = RecipeVariants diff --git a/packages/components/src/components/Stack/Stack.docs.mdx b/components/src/components/Stack/Stack.docs.mdx similarity index 98% rename from packages/components/src/components/Stack/Stack.docs.mdx rename to components/src/components/Stack/Stack.docs.mdx index a186342..9cc46fd 100644 --- a/packages/components/src/components/Stack/Stack.docs.mdx +++ b/components/src/components/Stack/Stack.docs.mdx @@ -4,7 +4,7 @@ description: Layout for elements --- ```tsx -import { Stack } from '@disco3/components'; +import { Stack } from 'degen' ``` ```tsx expand=true live=true diff --git a/packages/components/src/components/Stack/Stack.snippets.tsx b/components/src/components/Stack/Stack.snippets.tsx similarity index 53% rename from packages/components/src/components/Stack/Stack.snippets.tsx rename to components/src/components/Stack/Stack.snippets.tsx index 666b470..32b0686 100644 --- a/packages/components/src/components/Stack/Stack.snippets.tsx +++ b/components/src/components/Stack/Stack.snippets.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; +import * as React from 'react' -import { Snippet } from '!/playroom/src/types'; -import { Stack } from './Stack'; +import { Snippet } from '!/playroom/src/types' +import { Stack } from './Stack' export const snippets: Snippet[] = [ { @@ -12,4 +12,4 @@ export const snippets: Snippet[] = [ ), }, -]; +] diff --git a/components/src/components/Stack/Stack.test.tsx b/components/src/components/Stack/Stack.test.tsx new file mode 100644 index 0000000..45c1c94 --- /dev/null +++ b/components/src/components/Stack/Stack.test.tsx @@ -0,0 +1,19 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Box } from '../Box' +import { Stack } from './Stack' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render( + + foo bar baz + , + ) + expect(screen.getByText(/foo/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/Stack/Stack.tsx b/components/src/components/Stack/Stack.tsx similarity index 54% rename from packages/components/src/components/Stack/Stack.tsx rename to components/src/components/Stack/Stack.tsx index f2a537c..11de6dd 100644 --- a/packages/components/src/components/Stack/Stack.tsx +++ b/components/src/components/Stack/Stack.tsx @@ -1,9 +1,9 @@ -import * as React from 'react'; +import * as React from 'react' -import { OptionalResponsiveObject, OptionalResponsiveValue } from '../../css'; -import { ReactNodeNoStrings } from '../../types'; -import { Box, BoxProps } from '../Box'; -import { Direction, directionToFlexDirection, wrapToFlexWrap } from './utils'; +import { OptionalResponsiveObject, OptionalResponsiveValue } from '../../css' +import { ReactNodeNoStrings } from '../../types' +import { Box, BoxProps } from '../Box' +import { Direction, directionToFlexDirection, wrapToFlexWrap } from './utils' export const validStackComponents = [ 'a', @@ -16,18 +16,18 @@ export const validStackComponents = [ 'main', 'section', 'span', -] as const; +] as const type Props = { - as?: typeof validStackComponents[number]; - align?: BoxProps['alignItems']; - children: ReactNodeNoStrings; - direction?: OptionalResponsiveValue; - flex?: BoxProps['flex']; - justify?: BoxProps['justifyContent']; - space?: BoxProps['gap']; - wrap?: OptionalResponsiveObject; -}; + as?: typeof validStackComponents[number] + align?: BoxProps['alignItems'] + children: ReactNodeNoStrings + direction?: OptionalResponsiveValue + flex?: BoxProps['flex'] + justify?: BoxProps['justifyContent'] + space?: BoxProps['gap'] + wrap?: OptionalResponsiveObject +} export const Stack = ({ as = 'div', @@ -39,8 +39,8 @@ export const Stack = ({ space = '4', wrap, }: React.PropsWithChildren) => { - const flexDirection = directionToFlexDirection(direction); - const flexWrap = wrapToFlexWrap(wrap); + const flexDirection = directionToFlexDirection(direction) + const flexWrap = wrapToFlexWrap(wrap) return ( {children} - ); -}; + ) +} diff --git a/components/src/components/Stack/index.ts b/components/src/components/Stack/index.ts new file mode 100644 index 0000000..f0536fb --- /dev/null +++ b/components/src/components/Stack/index.ts @@ -0,0 +1 @@ +export { Stack } from './Stack' diff --git a/packages/components/src/components/Stack/utils.test.ts b/components/src/components/Stack/utils.test.ts similarity index 93% rename from packages/components/src/components/Stack/utils.test.ts rename to components/src/components/Stack/utils.test.ts index 8f82bd5..b40b656 100644 --- a/packages/components/src/components/Stack/utils.test.ts +++ b/components/src/components/Stack/utils.test.ts @@ -1,4 +1,4 @@ -import { directionToFlexDirection, wrapToFlexWrap } from './utils'; +import { directionToFlexDirection, wrapToFlexWrap } from './utils' describe.each` direction | expected @@ -10,9 +10,9 @@ describe.each` ${undefined} | ${undefined} `('directionToFlexDirection($direction)', ({ direction, expected }) => { it(`returns ${JSON.stringify(expected)}`, () => { - expect(directionToFlexDirection(direction)).toEqual(expected); - }); -}); + expect(directionToFlexDirection(direction)).toEqual(expected) + }) +}) describe.each` wrap | expected @@ -24,6 +24,6 @@ describe.each` ${undefined} | ${undefined} `('wrapToFlexWrap($wrap)', ({ wrap, expected }) => { it(`returns ${JSON.stringify(expected)}`, () => { - expect(wrapToFlexWrap(wrap)).toEqual(expected); - }); -}); + expect(wrapToFlexWrap(wrap)).toEqual(expected) + }) +}) diff --git a/packages/components/src/components/Stack/utils.ts b/components/src/components/Stack/utils.ts similarity index 85% rename from packages/components/src/components/Stack/utils.ts rename to components/src/components/Stack/utils.ts index cd2215d..e0fb00a 100644 --- a/packages/components/src/components/Stack/utils.ts +++ b/components/src/components/Stack/utils.ts @@ -2,13 +2,13 @@ import { OptionalResponsiveObject, OptionalResponsiveValue, mapResponsiveValue, -} from '../../css'; -export type Direction = 'horizontal' | 'vertical'; +} from '../../css' +export type Direction = 'horizontal' | 'vertical' const directionToFlexDirectionLookup = { horizontal: 'row', vertical: 'column', -} as const; +} as const export const directionToFlexDirection = ( direction: OptionalResponsiveValue | undefined, @@ -18,14 +18,14 @@ export const directionToFlexDirection = ( direction, (value) => directionToFlexDirectionLookup[value], ) - : undefined; + : undefined -type Wrap = 1 | 0; +type Wrap = 1 | 0 const wrapToFlexWrapLookup = { 1: 'wrap', 0: 'nowrap', -} as const; +} as const export const wrapToFlexWrap = ( wrap: OptionalResponsiveObject | undefined, @@ -39,4 +39,4 @@ export const wrapToFlexWrap = ( // boolean responsive keys (value) => wrapToFlexWrapLookup[+value as Wrap], ) - : undefined; + : undefined diff --git a/packages/components/src/components/Stat/Stat.docs.mdx b/components/src/components/Stat/Stat.docs.mdx similarity index 95% rename from packages/components/src/components/Stat/Stat.docs.mdx rename to components/src/components/Stat/Stat.docs.mdx index 2821dc6..33124d4 100644 --- a/packages/components/src/components/Stat/Stat.docs.mdx +++ b/components/src/components/Stat/Stat.docs.mdx @@ -4,7 +4,7 @@ description: Display a label, value, and sub-value --- ```tsx -import { Stat } from '@disco3/components'; +import { Stat } from 'degen' ``` ```tsx live=true expand=true diff --git a/packages/components/src/components/Stat/Stat.snippets.tsx b/components/src/components/Stat/Stat.snippets.tsx similarity index 59% rename from packages/components/src/components/Stat/Stat.snippets.tsx rename to components/src/components/Stat/Stat.snippets.tsx index 2cbbc33..c851a44 100644 --- a/packages/components/src/components/Stat/Stat.snippets.tsx +++ b/components/src/components/Stat/Stat.snippets.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; +import * as React from 'react' -import { Snippet } from '!/playroom/src/types'; -import { Stat } from './Stat'; +import { Snippet } from '!/playroom/src/types' +import { Stat } from './Stat' export const snippets: Snippet[] = [ { @@ -10,4 +10,4 @@ export const snippets: Snippet[] = [ ), }, -]; +] diff --git a/packages/components/src/components/Stat/Stat.test.tsx b/components/src/components/Stat/Stat.test.tsx similarity index 50% rename from packages/components/src/components/Stat/Stat.test.tsx rename to components/src/components/Stat/Stat.test.tsx index 76abe7b..13b4c42 100644 --- a/packages/components/src/components/Stat/Stat.test.tsx +++ b/components/src/components/Stat/Stat.test.tsx @@ -1,15 +1,15 @@ -import * as React from 'react'; +import * as React from 'react' -import { cleanup, render } from '@/test'; +import { cleanup, render } from '@/test' -import { Stat } from './Stat'; +import { Stat } from './Stat' describe('', () => { - afterEach(cleanup); + afterEach(cleanup) it('renders', () => { render( , - ); - }); -}); + ) + }) +}) diff --git a/packages/components/src/components/Stat/Stat.tsx b/components/src/components/Stat/Stat.tsx similarity index 59% rename from packages/components/src/components/Stat/Stat.tsx rename to components/src/components/Stat/Stat.tsx index 54dc5f1..88364ec 100644 --- a/packages/components/src/components/Stat/Stat.tsx +++ b/components/src/components/Stat/Stat.tsx @@ -1,15 +1,15 @@ -import * as React from 'react'; +import * as React from 'react' -import { Heading } from '../Heading'; -import { Stack } from '../Stack'; -import { Text } from '../Text'; +import { Heading } from '../Heading' +import { Stack } from '../Stack' +import { Text } from '../Text' type Props = { - label: React.ReactNode; - meta?: React.ReactNode; - size?: 'small' | 'medium'; - value: React.ReactNode; -}; + label: React.ReactNode + meta?: React.ReactNode + size?: 'small' | 'medium' + value: React.ReactNode +} export const Stat = ({ label, meta, size = 'medium', value }: Props) => { return ( @@ -20,11 +20,11 @@ export const Stat = ({ label, meta, size = 'medium', value }: Props) => { {value} ) : ( - + {value} )} {meta !== undefined && {meta}} - ); -}; + ) +} diff --git a/components/src/components/Stat/index.ts b/components/src/components/Stat/index.ts new file mode 100644 index 0000000..7811bc7 --- /dev/null +++ b/components/src/components/Stat/index.ts @@ -0,0 +1 @@ +export { Stat } from './Stat' diff --git a/packages/components/src/components/Tag/Tag.docs.mdx b/components/src/components/Tag/Tag.docs.mdx similarity index 91% rename from packages/components/src/components/Tag/Tag.docs.mdx rename to components/src/components/Tag/Tag.docs.mdx index fcf26a8..a16b128 100644 --- a/packages/components/src/components/Tag/Tag.docs.mdx +++ b/components/src/components/Tag/Tag.docs.mdx @@ -4,11 +4,11 @@ description: Indicator with optional label. --- ```tsx -import { Tag } from '@disco3/components'; +import { Tag } from 'degen' ``` ```tsx live=true expand=true -100 $FOLD +100 $WRITE ``` ## Props diff --git a/components/src/components/Tag/Tag.snippets.tsx b/components/src/components/Tag/Tag.snippets.tsx new file mode 100644 index 0000000..c00f1a7 --- /dev/null +++ b/components/src/components/Tag/Tag.snippets.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { Tag } from './Tag' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: Tag, + }, +] diff --git a/components/src/components/Tag/Tag.test.tsx b/components/src/components/Tag/Tag.test.tsx new file mode 100644 index 0000000..390ae31 --- /dev/null +++ b/components/src/components/Tag/Tag.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Tag } from './Tag' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render(10 ETH) + expect(screen.getByText(/eth/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/Tag/Tag.tsx b/components/src/components/Tag/Tag.tsx similarity index 79% rename from packages/components/src/components/Tag/Tag.tsx rename to components/src/components/Tag/Tag.tsx index adec6cb..b62d047 100644 --- a/packages/components/src/components/Tag/Tag.tsx +++ b/components/src/components/Tag/Tag.tsx @@ -1,12 +1,12 @@ -import * as React from 'react'; +import * as React from 'react' -import { Box } from '../Box'; -import * as styles from './styles.css'; +import { Box } from '../Box' +import * as styles from './styles.css' export type Props = { - as?: 'div' | 'span'; - label?: string; -} & styles.Variants; + as?: 'div' | 'span' + label?: string +} & styles.Variants export const Tag = ({ as = 'div', @@ -39,5 +39,5 @@ export const Tag = ({ {children} - ); -}; + ) +} diff --git a/components/src/components/Tag/index.ts b/components/src/components/Tag/index.ts new file mode 100644 index 0000000..e74ecce --- /dev/null +++ b/components/src/components/Tag/index.ts @@ -0,0 +1,2 @@ +export { Tag } from './Tag' +export type { Props as TagProps } from './Tag' diff --git a/packages/components/src/components/Tag/styles.css.ts b/components/src/components/Tag/styles.css.ts similarity index 85% rename from packages/components/src/components/Tag/styles.css.ts rename to components/src/components/Tag/styles.css.ts index fd57c9f..51b9afd 100644 --- a/packages/components/src/components/Tag/styles.css.ts +++ b/components/src/components/Tag/styles.css.ts @@ -1,15 +1,15 @@ -import { style } from '@vanilla-extract/css'; -import { CSSVarFunction } from '@vanilla-extract/private'; -import { RecipeVariants, recipe } from '@vanilla-extract/recipes'; +import { style } from '@vanilla-extract/css' +import { CSSVarFunction } from '@vanilla-extract/private' +import { RecipeVariants, recipe } from '@vanilla-extract/recipes' -import { atoms, rgb, vars } from '../../css'; +import { atoms, rgb, vars } from '../../css' export const label = style({ boxShadow: `0 0 0 2px ${vars.colors.background}`, -}); +}) const getToneHoverCompoundVariant = (tone: 'blue' | 'green' | 'red') => { - const color = vars.mode.colors[tone]; + const color = vars.mode.colors[tone] return { variants: { hover: true, @@ -25,11 +25,11 @@ const getToneHoverCompoundVariant = (tone: 'blue' | 'green' | 'red') => { }, }, }, - }; -}; + } +} const getToneColor = (color: CSSVarFunction) => - rgb(color, `calc(${vars.mode.shades.accentSecondary} * 0.5)`); + rgb(color, `calc(${vars.mode.shades.accentSecondary} * 0.5)`) export const variants = recipe({ base: [ @@ -93,6 +93,14 @@ export const variants = recipe({ backgroundColor: getToneColor(vars.mode.colors.red), }), ]), + orange: style([ + atoms({ + color: 'orange', + }), + style({ + backgroundColor: getToneColor(vars.mode.colors.orange), + }), + ]), purple: style([ atoms({ color: 'purple', @@ -143,6 +151,6 @@ export const variants = recipe({ getToneHoverCompoundVariant('green'), getToneHoverCompoundVariant('red'), ], -}); +}) -export type Variants = RecipeVariants; +export type Variants = RecipeVariants diff --git a/packages/components/src/components/Text/Text.docs.mdx b/components/src/components/Text/Text.docs.mdx similarity index 97% rename from packages/components/src/components/Text/Text.docs.mdx rename to components/src/components/Text/Text.docs.mdx index 0e22042..f9a4951 100644 --- a/packages/components/src/components/Text/Text.docs.mdx +++ b/components/src/components/Text/Text.docs.mdx @@ -4,7 +4,7 @@ description: Primitive typographic component --- ```tsx -import { Text } from '@disco3/components'; +import { Text } from 'degen' ``` ```tsx expand=true live=true diff --git a/components/src/components/Text/Text.snippets.tsx b/components/src/components/Text/Text.snippets.tsx new file mode 100644 index 0000000..8bdb8b5 --- /dev/null +++ b/components/src/components/Text/Text.snippets.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +import { Snippet } from '!/playroom/src/types' +import { Text } from './Text' + +export const snippets: Snippet[] = [ + { + name: 'Basic', + code: Text, + }, +] diff --git a/components/src/components/Text/Text.test.tsx b/components/src/components/Text/Text.test.tsx new file mode 100644 index 0000000..959fb70 --- /dev/null +++ b/components/src/components/Text/Text.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' + +import { cleanup, render, screen } from '@/test' + +import { Text } from './Text' + +describe('', () => { + afterEach(cleanup) + + it('renders', () => { + render(foo bar baz) + expect(screen.getByText(/foo/i)).toBeInTheDocument() + }) +}) diff --git a/packages/components/src/components/Text/Text.tsx b/components/src/components/Text/Text.tsx similarity index 61% rename from packages/components/src/components/Text/Text.tsx rename to components/src/components/Text/Text.tsx index 3a0da34..6647fb2 100644 --- a/packages/components/src/components/Text/Text.tsx +++ b/components/src/components/Text/Text.tsx @@ -1,10 +1,10 @@ -import * as React from 'react'; +import * as React from 'react' -import { Box, BoxProps } from '../Box'; -import * as styles from './styles.css'; +import { Box, BoxProps } from '../Box' +import * as styles from './styles.css' type Props = { - align?: BoxProps['textAlign']; + align?: BoxProps['textAlign'] as?: | 'code' | 'div' @@ -16,17 +16,18 @@ type Props = { | 'h6' | 'label' | 'p' - | 'span'; - children?: React.ReactNode; - color?: BoxProps['color']; - font?: BoxProps['fontFamily']; - letterSpacing?: BoxProps['letterSpacing']; - lineHeight?: BoxProps['lineHeight']; - size?: BoxProps['fontSize']; - transform?: BoxProps['textTransform']; - weight?: BoxProps['fontWeight']; - whiteSpace?: BoxProps['whiteSpace']; -} & styles.Variants; + | 'span' + children?: React.ReactNode + color?: BoxProps['color'] + font?: BoxProps['fontFamily'] + letterSpacing?: BoxProps['letterSpacing'] + lineHeight?: BoxProps['lineHeight'] + size?: BoxProps['fontSize'] + transform?: BoxProps['textTransform'] + weight?: BoxProps['fontWeight'] + whiteSpace?: BoxProps['whiteSpace'] + wordBreak?: BoxProps['wordBreak'] +} & styles.Variants export const Text = React.forwardRef( ( @@ -44,6 +45,7 @@ export const Text = React.forwardRef( variant, weight, whiteSpace, + wordBreak, }: Props, ref: React.Ref, ) => { @@ -64,11 +66,12 @@ export const Text = React.forwardRef( textAlign={align} textTransform={transform} whiteSpace={whiteSpace} + wordBreak={wordBreak} > {children} - ); + ) }, -); +) -Text.displayName = 'Text'; +Text.displayName = 'Text' diff --git a/components/src/components/Text/index.ts b/components/src/components/Text/index.ts new file mode 100644 index 0000000..c79f9ee --- /dev/null +++ b/components/src/components/Text/index.ts @@ -0,0 +1 @@ +export { Text } from './Text' diff --git a/packages/components/src/components/Text/styles.css.ts b/components/src/components/Text/styles.css.ts similarity index 88% rename from packages/components/src/components/Text/styles.css.ts rename to components/src/components/Text/styles.css.ts index 9df8d39..11dbf6f 100644 --- a/packages/components/src/components/Text/styles.css.ts +++ b/components/src/components/Text/styles.css.ts @@ -1,7 +1,7 @@ -import { style } from '@vanilla-extract/css'; -import { RecipeVariants, recipe } from '@vanilla-extract/recipes'; +import { style } from '@vanilla-extract/css' +import { RecipeVariants, recipe } from '@vanilla-extract/recipes' -import { atoms } from '../../css'; +import { atoms } from '../../css' const variant = { extraLarge: style([ @@ -47,7 +47,7 @@ const variant = { letterSpacing: '0.03', textTransform: 'uppercase', }), -}; +} export const variants = recipe({ variants: { @@ -64,6 +64,6 @@ export const variants = recipe({ ]), }, }, -}); +}) -export type Variants = RecipeVariants; +export type Variants = RecipeVariants diff --git a/packages/components/src/components/Textarea/Textarea.docs.mdx b/components/src/components/Textarea/Textarea.docs.mdx similarity index 53% rename from packages/components/src/components/Textarea/Textarea.docs.mdx rename to components/src/components/Textarea/Textarea.docs.mdx index e46b4d7..1b04ef6 100644 --- a/packages/components/src/components/Textarea/Textarea.docs.mdx +++ b/components/src/components/Textarea/Textarea.docs.mdx @@ -4,13 +4,13 @@ description: Surface for accepting multi-line user input --- ```tsx -import { Textarea } from '@disco3/components'; +import { Textarea } from 'degen' ``` ```tsx live=true expand=true