The build system has been migrated from rollup to pridepack, and the propTypes for the components have been deprecated, as the prop typing is now done in TypeScript type declarations. The documentation is in the process of migration from Next to Storybook. Issues with docgen remain for components with forwarded refs.refactor/pridepack-storybook
@@ -1,9 +0,0 @@ | |||||
# If we are using TypeScript? | |||||
DOCZ_TYPESCRIPT= | |||||
# Base path of the routes | |||||
DOCZ_BASE= | |||||
# Port where Docz runs | |||||
DOCZ_PORT= | |||||
@@ -5,7 +5,6 @@ import unified from 'unified' | |||||
import parse from 'remark-parse' | import parse from 'remark-parse' | ||||
import remark2react from 'remark-react' | import remark2react from 'remark-react' | ||||
import docgen from '../../docgen.json' | import docgen from '../../docgen.json' | ||||
import pkg from '../../../../../package.json' | |||||
const propTypes = { | const propTypes = { | ||||
of: PropTypes.string, | of: PropTypes.string, | ||||
@@ -35,7 +34,7 @@ const Header: React.FC<Props> = ({ of: ofAttr }) => { | |||||
<React.Fragment> | <React.Fragment> | ||||
<Head> | <Head> | ||||
<title> | <title> | ||||
{docs.displayName} | {pkg['title'] || pkg.name} | |||||
{docs.displayName} | {process.env.NEXT_PUBLIC_APP_TITLE} | |||||
</title> | </title> | ||||
<meta name="description" content={summary} key="description" /> | <meta name="description" content={summary} key="description" /> | ||||
</Head> | </Head> | ||||
@@ -1,7 +1,6 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import * as PropTypes from 'prop-types' | import * as PropTypes from 'prop-types' | ||||
import { Icon } from '../../../../react-common/src' | import { Icon } from '../../../../react-common/src' | ||||
import pkg from '../../../../../package.json' | |||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
import Link from 'next/link' | import Link from 'next/link' | ||||
import Nav from '../Nav/Nav' | import Nav from '../Nav/Nav' | ||||
@@ -202,7 +201,7 @@ const Sidebar: React.FC<Props> = ({ | |||||
initialTheme={initialTheme} | initialTheme={initialTheme} | ||||
/> | /> | ||||
<RepoLink | <RepoLink | ||||
href={pkg.repository} | |||||
href={process.env.NEXT_PUBLIC_APP_REPOSITORY} | |||||
target="_blank" | target="_blank" | ||||
rel="noopener noreferrer" | rel="noopener noreferrer" | ||||
> | > | ||||
@@ -10,7 +10,7 @@ import Header from '../../components/Header/Header' | |||||
<Header of="TextInput" /> | <Header of="TextInput" /> | ||||
<Playground | <Playground | ||||
components={{ TextInput }} | |||||
components={{ TextInput: Index }} | |||||
code={` | code={` | ||||
<TextInput | <TextInput | ||||
label="Username" | label="Username" | ||||
@@ -35,7 +35,7 @@ The components are surrounded by `inline-block` elements. These surrounding elem | |||||
act as guide to the user on how long the expected input values are. | act as guide to the user on how long the expected input values are. | ||||
<Playground | <Playground | ||||
components={{ TextInput }} | |||||
components={{ TextInput: Index }} | |||||
code={` | code={` | ||||
<form> | <form> | ||||
I am <span style={{ display: 'inline-block', width: '12rem', verticalAlign: 'bottom', }}><TextInput label="Full name" hint="given and family name" /></span> and I live in <span style={{ display: 'inline-block', width: '16rem', verticalAlign: 'bottom', }}><TextInput label="Address" hint="city, state and country" /></span>. | I am <span style={{ display: 'inline-block', width: '12rem', verticalAlign: 'bottom', }}><TextInput label="Full name" hint="given and family name" /></span> and I live in <span style={{ display: 'inline-block', width: '16rem', verticalAlign: 'bottom', }}><TextInput label="Address" hint="city, state and country" /></span>. | ||||
@@ -51,7 +51,7 @@ some content that is best displayed outside the component instead of putting in | |||||
<Playground | <Playground | ||||
label="Example shipping address form" | label="Example shipping address form" | ||||
components={{ TextInput }} | |||||
components={{ TextInput: Index }} | |||||
code={` | code={` | ||||
<form | <form | ||||
style={{ display: 'grid', gridTemplateColumns: '4fr 4fr 5fr', gap: '1rem', }} | style={{ display: 'grid', gridTemplateColumns: '4fr 4fr 5fr', gap: '1rem', }} | ||||
@@ -0,0 +1,9 @@ | |||||
{ | |||||
"root": true, | |||||
"extends": [ | |||||
"lxsmnsyc/typescript/react" | |||||
], | |||||
"parserOptions": { | |||||
"project": "./tsconfig.eslint.json" | |||||
} | |||||
} |
@@ -0,0 +1,107 @@ | |||||
# Logs | |||||
logs | |||||
*.log | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
lerna-debug.log* | |||||
# Diagnostic reports (https://nodejs.org/api/report.html) | |||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |||||
# Runtime data | |||||
pids | |||||
*.pid | |||||
*.seed | |||||
*.pid.lock | |||||
# Directory for instrumented libs generated by jscoverage/JSCover | |||||
lib-cov | |||||
# Coverage directory used by tools like istanbul | |||||
coverage | |||||
*.lcov | |||||
# nyc test coverage | |||||
.nyc_output | |||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | |||||
.grunt | |||||
# Bower dependency directory (https://bower.io/) | |||||
bower_components | |||||
# node-waf configuration | |||||
.lock-wscript | |||||
# Compiled binary addons (https://nodejs.org/api/addons.html) | |||||
build/Release | |||||
# Dependency directories | |||||
node_modules/ | |||||
jspm_packages/ | |||||
# TypeScript v1 declaration files | |||||
typings/ | |||||
# TypeScript cache | |||||
*.tsbuildinfo | |||||
# Optional npm cache directory | |||||
.npm | |||||
# Optional eslint cache | |||||
.eslintcache | |||||
# Microbundle cache | |||||
.rpt2_cache/ | |||||
.rts2_cache_cjs/ | |||||
.rts2_cache_es/ | |||||
.rts2_cache_umd/ | |||||
# Optional REPL history | |||||
.node_repl_history | |||||
# Output of 'npm pack' | |||||
*.tgz | |||||
# Yarn Integrity file | |||||
.yarn-integrity | |||||
# dotenv environment variables file | |||||
.env | |||||
.env.production | |||||
.env.development | |||||
# parcel-bundler cache (https://parceljs.org/) | |||||
.cache | |||||
# Next.js build output | |||||
.next | |||||
# Nuxt.js build / generate output | |||||
.nuxt | |||||
dist | |||||
# Gatsby files | |||||
.cache/ | |||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js | |||||
# https://nextjs.org/blog/next-9-1#public-directory-support | |||||
# public | |||||
# vuepress build output | |||||
.vuepress/dist | |||||
# Serverless directories | |||||
.serverless/ | |||||
# FuseBox cache | |||||
.fusebox/ | |||||
# DynamoDB Local files | |||||
.dynamodb/ | |||||
# TernJS port file | |||||
.tern-port | |||||
.npmrc |
@@ -8,7 +8,6 @@ utilities/ | |||||
jest.config.js | jest.config.js | ||||
jest.setup.ts | jest.setup.ts | ||||
plopfile.js | plopfile.js | ||||
rollup.config.js | |||||
tsconfig.json | tsconfig.json | ||||
yarn.lock | yarn.lock | ||||
**/*.test.d.ts | **/*.test.d.ts |
@@ -0,0 +1,22 @@ | |||||
module.exports = { | |||||
//typescript: { | |||||
// reactDocgen: 'react-docgen', | |||||
//}, | |||||
stories: [ | |||||
"../docs/**/*.mdx", | |||||
"../src/**/*.stories.@(js|jsx|ts|tsx)" | |||||
], | |||||
addons: [ | |||||
"@storybook/addon-links", | |||||
"@storybook/addon-essentials", | |||||
{ | |||||
name: '@storybook/addon-docs', | |||||
options: { | |||||
configureJSX: true, | |||||
babelOptions: {}, | |||||
sourceLoaderOptions: null, | |||||
transcludeMarkdown: true, | |||||
} | |||||
}, | |||||
] | |||||
} |
@@ -0,0 +1,5 @@ | |||||
<style> | |||||
:root { | |||||
font-family: var(--font-family-base, sans-serif); | |||||
} | |||||
</style> |
@@ -0,0 +1,19 @@ | |||||
export const parameters = { | |||||
actions: { argTypesRegex: "^on[A-Z].*" }, | |||||
controls: { | |||||
matchers: { | |||||
color: /(background|color)$/i, | |||||
date: /Date$/, | |||||
}, | |||||
backgrounds: { | |||||
grid: { | |||||
cellSize: 16, | |||||
opacity: 0.5, | |||||
cellAmount: 5, | |||||
offsetX: 16, // default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded' | |||||
offsetY: 16, // default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded' | |||||
}, | |||||
}, | |||||
}, | |||||
} |
@@ -0,0 +1,9 @@ | |||||
import { Meta } from '@storybook/addon-docs/blocks'; | |||||
import Readme from '../README.md'; | |||||
<Meta | |||||
title="Introduction" | |||||
parameters={{ previewTabs: { canvas: { hidden: true } } }} | |||||
/> | |||||
<Readme /> |
@@ -0,0 +1,100 @@ | |||||
import { Meta } from '@storybook/addon-docs' | |||||
<Meta | |||||
title="Theming" | |||||
parameters={{ previewTabs: { canvas: { hidden: true } } }} | |||||
/> | |||||
# Theming | |||||
The components can be styled through CSS custom properties, meaning browsers that support custom properties are expected | |||||
to support theming for Tesseract components. It is a huge advantage for Tesseract to have used this feature because of | |||||
its ease of use upon application, compared to using preprocessors such as Sass, which employs its own language as well | |||||
as potentially increasing the size of the bundled stylesheets. | |||||
## Properties | |||||
Tesseract uses a few properties as much as possible in order to produce lightweight themes. | |||||
### Colors | |||||
#### `color-accent` | |||||
Default value: `blue` | |||||
The accent color of the components. Used to designate components that can be interacted with. | |||||
#### `color-fg` | |||||
Default value: `black` | |||||
The reference color for text, borders, and anything in the foreground. | |||||
#### `color-bg` | |||||
Default value: `white` | |||||
The reference color for anything in the background. | |||||
#### `color-active` | |||||
Default value: `Highlight` | |||||
The version of the accent color for activated (i.e. clicked or focused) components. | |||||
### Fonts | |||||
#### `font-family-base` | |||||
Default value: `sans-serif` | |||||
The base font family. Used for body text. | |||||
## Usage | |||||
You should have a global theme rule applied to `:root`. Below is an example of using a global theme to apply styles: | |||||
```css title=theme.css | |||||
:root { | |||||
/* This will be our reference background color */ | |||||
--color-negative: #eee; | |||||
/* This will be our reference foreground color */ | |||||
--color-positive: #222; | |||||
--color-accent: #ba6a9c; | |||||
--color-active: #f90; | |||||
--font-family-body: 'Encode Sans Semi Expanded', 'Encode Sans', system-ui; | |||||
} | |||||
@media (prefers-color-scheme: dark) { | |||||
/* Dark mode, because why not? */ | |||||
:root { | |||||
--color-negative: #222; | |||||
--color-positive: #eee; | |||||
--color-accent: #C78AB3; | |||||
} | |||||
} | |||||
:root { | |||||
/* We use our reference colors to set global fg and bg, but we could apply them to other elements later on. | |||||
* | |||||
* See below for example usage. | |||||
*/ | |||||
--color-bg: var(--color-negative, white); | |||||
--color-fg: var(--color-positive, black); | |||||
background-color: var(--color-bg); | |||||
color: var(--color-fg); | |||||
font-family: var(--font-family-body, sans-serif), sans-serif; | |||||
} | |||||
/* | |||||
* Here is an example usage of overriding bg and fg. | |||||
*/ | |||||
.inverted-colors { | |||||
--color-bg: var(--color-positive, black); | |||||
--color-fg: var(--color-negative, white); | |||||
} | |||||
``` | |||||
Since the components use the same styles, the created theme will be applied to them as well. |
@@ -1,3 +1,3 @@ | |||||
import 'jest-enzyme' | import 'jest-enzyme' | ||||
import 'jest-extended' | import 'jest-extended' | ||||
import './utilities/jest/extensions' | |||||
import '../../utilities/jest/extensions' |
@@ -0,0 +1,78 @@ | |||||
{ | |||||
"version": "0.5.0", | |||||
"types": "dist/types/index.d.ts", | |||||
"main": "dist/cjs/index.js", | |||||
"module": "dist/esm/index.js", | |||||
"exports": { | |||||
"require": "./dist/cjs/index.js", | |||||
"import": "./dist/esm/index.js" | |||||
}, | |||||
"files": [ | |||||
"dist", | |||||
"src" | |||||
], | |||||
"engines": { | |||||
"node": ">=10" | |||||
}, | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||||
"license": "MIT", | |||||
"private": false, | |||||
"bugs": { | |||||
"url": "https://code.modal.sh/tesseract-design/react-common/issues" | |||||
}, | |||||
"keywords": [ | |||||
"components", | |||||
"react" | |||||
], | |||||
"name": "@tesseract-design/react-common", | |||||
"homepage": "https://make.modal.sh/tesseract/web/react/common", | |||||
"title": "React Common", | |||||
"org": "Tesseract Design", | |||||
"description": "Common front-end components for Web using the Tesseract design system, written in React.", | |||||
"repository": "https://code.modal.sh/tesseract-design/react-common.git", | |||||
"devDependencies": { | |||||
"@babel/core": "^7.15.0", | |||||
"@storybook/addon-actions": "^6.3.7", | |||||
"@storybook/addon-essentials": "^6.3.7", | |||||
"@storybook/addon-links": "^6.3.7", | |||||
"@storybook/react": "^6.3.7", | |||||
"@testing-library/jest-dom": "^5.14.1", | |||||
"@testing-library/react": "^12.0.0", | |||||
"@testing-library/react-hooks": "^7.0.1", | |||||
"@types/jest": "^27.0.1", | |||||
"@types/node": "^16.6.1", | |||||
"@types/react": "^17.0.17", | |||||
"@types/styled-components": "^5.1.12", | |||||
"babel-loader": "^8.2.2", | |||||
"eslint": "^7.32.0", | |||||
"eslint-config-lxsmnsyc": "^0.2.3", | |||||
"pridepack": "^0.10.0", | |||||
"react": "^17.0.2", | |||||
"react-dom": "^17.0.2", | |||||
"react-test-renderer": "^17.0.2", | |||||
"styled-components": "^5.3.0", | |||||
"tslib": "^2.3.1", | |||||
"typescript": "^4.3.5" | |||||
}, | |||||
"peerDependencies": { | |||||
"react": "^16.8.0 || ^17.0.0", | |||||
"react-dom": "^16.8.0 || ^17.0.0", | |||||
"styled-components": "^5.3.0" | |||||
}, | |||||
"scripts": { | |||||
"prepublish": "pridepack clean && pridepack build", | |||||
"build": "pridepack build", | |||||
"type-check": "pridepack check", | |||||
"lint": "pridepack lint", | |||||
"test": "pridepack test --passWithNoTests", | |||||
"clean": "pridepack clean", | |||||
"watch": "pridepack watch", | |||||
"storybook": "start-storybook -p 6006", | |||||
"build-storybook": "build-storybook" | |||||
}, | |||||
"dependencies": { | |||||
"@reach/slider": "^0.16.0", | |||||
"pascal-case": "^3.1.2", | |||||
"react-feather": "^2.0.9" | |||||
} | |||||
} |
@@ -0,0 +1,6 @@ | |||||
{ | |||||
"jest": { | |||||
"testEnvironment": "jsdom" | |||||
}, | |||||
"target": "es2017" | |||||
} |
@@ -0,0 +1,95 @@ | |||||
import * as React from 'react' | |||||
import { Button, ButtonElement, ButtonSize, ButtonVariant, Props } from '.' | |||||
import { Meta, Story } from '@storybook/react' | |||||
const meta: Meta = { | |||||
title: 'components/Button', | |||||
component: Button, | |||||
argTypes: { onClick: { action: 'clicked' } }, | |||||
} | |||||
const Template: Story<Props> = ({ ref, ...args }) => ( | |||||
<Button | |||||
{...args} | |||||
/> | |||||
) | |||||
export const Default = Template.bind({}) | |||||
Default.args = { | |||||
children: 'Button', | |||||
} | |||||
export const Primary = Template.bind({}) | |||||
Primary.args = { | |||||
children: 'Button', | |||||
border: true, | |||||
variant: ButtonVariant.PRIMARY, | |||||
} | |||||
export const Border = Template.bind({}) | |||||
Border.args = { | |||||
children: 'Button', | |||||
border: true, | |||||
} | |||||
export const Disabled = Template.bind({}) | |||||
Disabled.args = { | |||||
children: 'Button', | |||||
border: true, | |||||
disabled: true, | |||||
} | |||||
export const DisabledLink = Template.bind({}) | |||||
DisabledLink.args = { | |||||
children: 'Button', | |||||
border: true, | |||||
disabled: true, | |||||
element: ButtonElement.A, | |||||
href: 'https://www.example.com', | |||||
} | |||||
export const Link = Template.bind({}) | |||||
Link.args = { | |||||
children: 'Button', | |||||
border: true, | |||||
element: ButtonElement.A, | |||||
href: 'https://www.example.com', | |||||
} | |||||
export const Small = Template.bind({}) | |||||
Small.args = { | |||||
children: 'Button', | |||||
border: true, | |||||
size: ButtonSize.SMALL, | |||||
} | |||||
export const Large = Template.bind({}) | |||||
Large.args = { | |||||
children: 'Button', | |||||
border: true, | |||||
size: ButtonSize.LARGE, | |||||
} | |||||
export const Block = Template.bind({}) | |||||
Block.args = { | |||||
children: 'Button', | |||||
border: true, | |||||
block: true, | |||||
} | |||||
export const DisabledWithinFieldset: Story<Props> = ({ ref, disabled, ...args }) => ( | |||||
<fieldset | |||||
disabled={disabled} | |||||
> | |||||
<Button | |||||
{...args} | |||||
/> | |||||
</fieldset> | |||||
) | |||||
DisabledWithinFieldset.args = { | |||||
children: 'Button', | |||||
border: true, | |||||
disabled: true, | |||||
} | |||||
export default meta |
@@ -5,7 +5,7 @@ import * as fc from 'fast-check' | |||||
import * as Enzyme from 'enzyme' | import * as Enzyme from 'enzyme' | ||||
import * as Axe from 'jest-axe' | import * as Axe from 'jest-axe' | ||||
import * as React from 'react' | import * as React from 'react' | ||||
import Button, { Variant, ButtonElement } from './Button' | |||||
import Button, { Variant, ButtonElement } from '.' | |||||
import stringify from '../../utilities/stringify' | import stringify from '../../utilities/stringify' | ||||
const CUSTOM_VARIANTS: string[] = ['primary'] | const CUSTOM_VARIANTS: string[] = ['primary'] | ||||
@@ -1,19 +1,32 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import * as PropTypes from 'prop-types' | |||||
import styled, { CSSObject } from 'styled-components' | import styled, { CSSObject } from 'styled-components' | ||||
import { Size, SizeMap } from '../../utilities/utilities' | |||||
import stringify from '../../utilities/stringify' | |||||
export type Variant = 'outline' | 'primary' | |||||
export enum ButtonSize { | |||||
SMALL = 'small', | |||||
MEDIUM = 'medium', | |||||
LARGE = 'large', | |||||
} | |||||
export enum ButtonVariant { | |||||
OUTLINE = 'outline', | |||||
PRIMARY = 'primary', | |||||
} | |||||
export type ButtonElement = 'a' | 'button' | |||||
export enum ButtonElement { | |||||
A = 'a', | |||||
BUTTON = 'button', | |||||
} | |||||
type ButtonType = 'submit' | 'reset' | 'button' | |||||
export enum ButtonType { | |||||
SUBMIT = 'submit', | |||||
RESET = 'reset', | |||||
BUTTON = 'button', | |||||
} | |||||
const MIN_HEIGHTS: SizeMap<string | number> = { | |||||
small: '2.5rem', | |||||
medium: '3rem', | |||||
large: '4rem', | |||||
const MIN_HEIGHTS: Record<ButtonSize, string | number> = { | |||||
[ButtonSize.SMALL]: '2.5rem', | |||||
[ButtonSize.MEDIUM]: '3rem', | |||||
[ButtonSize.LARGE]: '4rem', | |||||
} | } | ||||
const disabledButtonStyles: CSSObject = { | const disabledButtonStyles: CSSObject = { | ||||
@@ -22,7 +35,7 @@ const disabledButtonStyles: CSSObject = { | |||||
} | } | ||||
const buttonStyles: CSSObject = { | const buttonStyles: CSSObject = { | ||||
display: 'grid', | |||||
display: 'inline-grid', | |||||
appearance: 'none', | appearance: 'none', | ||||
padding: '0 1rem', | padding: '0 1rem', | ||||
font: 'inherit', | font: 'inherit', | ||||
@@ -57,7 +70,6 @@ const disabledLinkButtonStyles: CSSObject = { | |||||
const Base = styled('button')({ | const Base = styled('button')({ | ||||
...buttonStyles, | ...buttonStyles, | ||||
width: '100%', | |||||
}) | }) | ||||
Base.displayName = 'button' | Base.displayName = 'button' | ||||
@@ -107,7 +119,7 @@ const defaultVariantStyleSet: React.CSSProperties = { | |||||
color: 'var(--color-accent, blue)', | color: 'var(--color-accent, blue)', | ||||
} | } | ||||
const variantStyleSets: Record<Variant, React.CSSProperties> = { | |||||
const variantStyleSets: Record<ButtonVariant, React.CSSProperties> = { | |||||
outline: defaultVariantStyleSet, | outline: defaultVariantStyleSet, | ||||
primary: { | primary: { | ||||
backgroundColor: 'var(--color-accent, blue)', | backgroundColor: 'var(--color-accent, blue)', | ||||
@@ -115,132 +127,101 @@ const variantStyleSets: Record<Variant, React.CSSProperties> = { | |||||
}, | }, | ||||
} | } | ||||
const propTypes = { | |||||
export type Props = Omit<React.HTMLProps<HTMLButtonElement>, 'as' | 'size'> & Omit<React.HTMLProps<HTMLAnchorElement>, 'as' | 'size'> & { | |||||
/** | /** | ||||
* Size of the component. | * Size of the component. | ||||
*/ | */ | ||||
size: PropTypes.oneOf<Size>(['small', 'medium', 'large']), | |||||
size?: ButtonSize, | |||||
/** | /** | ||||
* Variant of the component. | * Variant of the component. | ||||
*/ | */ | ||||
variant: PropTypes.oneOf<Variant>(['outline', 'primary']), | |||||
/** | |||||
* Text to identify the action associated upon activation of the component. | |||||
*/ | |||||
children: PropTypes.any, | |||||
variant?: ButtonVariant, | |||||
/** | /** | ||||
* Can the component be activated? | * Can the component be activated? | ||||
*/ | */ | ||||
disabled: PropTypes.bool, | |||||
disabled?: boolean, | |||||
/** | /** | ||||
* The corresponding HTML element of the component. | * The corresponding HTML element of the component. | ||||
*/ | */ | ||||
element: PropTypes.oneOf<ButtonElement>(['a', 'button']), | |||||
/** | |||||
* The URL of the page to navigate to, if `element` is set to `"a"`. | |||||
*/ | |||||
href: PropTypes.string, | |||||
/** | |||||
* The target on where to display the page navigated to, if `element` is set to `"a"`. | |||||
*/ | |||||
target: PropTypes.string, | |||||
element?: ButtonElement, | |||||
/** | /** | ||||
* The relationship of the current page to the referred page in `href`, if `element` is set to `"a"`. | |||||
* Should the component display a border? | |||||
*/ | */ | ||||
rel: PropTypes.string, | |||||
border?: boolean, | |||||
/** | /** | ||||
* The type of the button, if `element` is set to `"button"`. | |||||
* Should the component occupy the whole width of its parent? | |||||
*/ | */ | ||||
type: PropTypes.oneOf<ButtonType>(['submit', 'reset', 'button']), | |||||
/** | |||||
* Does the button display a border? | |||||
*/ | |||||
border: PropTypes.bool, | |||||
/** | |||||
* Event handler triggered when the component is clicked. | |||||
*/ | |||||
onClick: PropTypes.func, | |||||
/** | |||||
* Event handler triggered when the component receives focus. | |||||
*/ | |||||
onFocus: PropTypes.func, | |||||
/** | |||||
* Event handler triggered when the component loses focus. | |||||
*/ | |||||
onBlur: PropTypes.func, | |||||
block?: boolean, | |||||
} | } | ||||
type Props = PropTypes.InferProps<typeof propTypes> | |||||
type RefElement = HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement | |||||
/** | /** | ||||
* Component for performing an action upon activation (e.g. when clicked). | * Component for performing an action upon activation (e.g. when clicked). | ||||
* | * | ||||
* Depending on the declared props, this component can function as a hyperlink or as a button. | * Depending on the declared props, this component can function as a hyperlink or as a button. | ||||
*/ | */ | ||||
const Button = React.forwardRef<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement, Props>( | |||||
export const Button = React.forwardRef<RefElement, Props>( | |||||
( | ( | ||||
{ | { | ||||
size = 'medium', | |||||
variant = 'outline', | |||||
size = ButtonSize.MEDIUM, | |||||
variant = ButtonVariant.OUTLINE, | |||||
disabled = false, | disabled = false, | ||||
children, | |||||
element = 'button', | |||||
href, | |||||
target, | |||||
rel, | |||||
type = 'button', | |||||
element = ButtonElement.BUTTON, | |||||
border = false, | border = false, | ||||
onClick, | |||||
onFocus, | |||||
onBlur, | |||||
children, | |||||
type = ButtonType.BUTTON, | |||||
block = false, | |||||
className: _className, | |||||
style: _style, | |||||
...etcProps | |||||
}, | }, | ||||
ref, | ref, | ||||
) => { | ) => { | ||||
const { [variant as Variant]: theVariantStyleSet = defaultVariantStyleSet } = variantStyleSets | |||||
const { [variant]: theVariantStyleSet = defaultVariantStyleSet } = variantStyleSets | |||||
const commonButtonStyles: React.CSSProperties = { | const commonButtonStyles: React.CSSProperties = { | ||||
...theVariantStyleSet, | ...theVariantStyleSet, | ||||
minHeight: MIN_HEIGHTS[size!], | minHeight: MIN_HEIGHTS[size!], | ||||
width: block ? '100%' : undefined, | |||||
} | } | ||||
const buttonContent = ( | const buttonContent = ( | ||||
<React.Fragment> | |||||
<> | |||||
{border && <Border />} | {border && <Border />} | ||||
{stringify(children)} | |||||
</React.Fragment> | |||||
{children} | |||||
</> | |||||
) | ) | ||||
switch (element) { | switch (element) { | ||||
case 'button': | |||||
case ButtonElement.BUTTON: | |||||
return ( | return ( | ||||
<Base | <Base | ||||
onClick={onClick as React.EventHandler<React.SyntheticEvent>} | |||||
type={type!} | |||||
{...etcProps} | |||||
ref={ref as React.Ref<HTMLButtonElement>} | ref={ref as React.Ref<HTMLButtonElement>} | ||||
disabled={disabled!} | disabled={disabled!} | ||||
style={commonButtonStyles} | style={commonButtonStyles} | ||||
onFocus={onFocus as React.FocusEventHandler} | |||||
onBlur={onBlur as React.FocusEventHandler} | |||||
type={type as (ButtonType | undefined)} | |||||
> | > | ||||
{buttonContent} | {buttonContent} | ||||
</Base> | </Base> | ||||
) | ) | ||||
case 'a': | |||||
case ButtonElement.A: | |||||
if (disabled) { | if (disabled) { | ||||
return ( | return ( | ||||
<DisabledLinkBase ref={ref as React.Ref<HTMLSpanElement>} style={commonButtonStyles}> | |||||
<DisabledLinkBase | |||||
ref={ref as React.Ref<HTMLSpanElement>} | |||||
style={commonButtonStyles} | |||||
role="link" | |||||
aria-disabled="true" | |||||
> | |||||
{buttonContent} | {buttonContent} | ||||
</DisabledLinkBase> | </DisabledLinkBase> | ||||
) | ) | ||||
} | } | ||||
return ( | return ( | ||||
<LinkBase | <LinkBase | ||||
onClick={onClick as React.EventHandler<React.SyntheticEvent>} | |||||
href={href!} | |||||
target={target!} | |||||
rel={rel!} | |||||
{...etcProps} | |||||
ref={ref as React.Ref<HTMLAnchorElement>} | ref={ref as React.Ref<HTMLAnchorElement>} | ||||
style={commonButtonStyles} | style={commonButtonStyles} | ||||
onFocus={onFocus as React.FocusEventHandler} | |||||
onBlur={onBlur as React.FocusEventHandler} | |||||
> | > | ||||
{buttonContent} | {buttonContent} | ||||
</LinkBase> | </LinkBase> | ||||
@@ -253,8 +234,4 @@ const Button = React.forwardRef<HTMLAnchorElement | HTMLButtonElement | HTMLSpan | |||||
}, | }, | ||||
) | ) | ||||
Button.propTypes = propTypes | |||||
Button.displayName = 'Button' | Button.displayName = 'Button' | ||||
export default Button |
@@ -0,0 +1,56 @@ | |||||
import * as React from 'react' | |||||
import { Checkbox, Props } from '.' | |||||
import { Meta, Story } from '@storybook/react' | |||||
const meta: Meta = { | |||||
title: 'components/Checkbox', | |||||
component: Checkbox, | |||||
argTypes: { onChange: { action: 'changed' } }, | |||||
} | |||||
const Template: Story<Props> = ({ ref, ...args }) => { | |||||
return ( | |||||
<> | |||||
Some text | |||||
{' '} | |||||
<Checkbox | |||||
{...args} | |||||
/> | |||||
{' '} | |||||
Another text | |||||
</> | |||||
) | |||||
} | |||||
export const Default = Template.bind({}) | |||||
Default.args = { | |||||
label: 'Checkbox label', | |||||
} | |||||
export const Disabled = Template.bind({}) | |||||
Disabled.args = { | |||||
label: 'Checkbox label', | |||||
disabled: true, | |||||
} | |||||
export const HiddenLabel = Template.bind({}) | |||||
HiddenLabel.args = { | |||||
label: 'Checkbox label', | |||||
hiddenLabel: true, | |||||
} | |||||
export const DisabledWithinFieldset: Story<Props> = ({ ref, disabled, ...args }) => ( | |||||
<fieldset | |||||
disabled={disabled} | |||||
> | |||||
<Checkbox | |||||
{...args} | |||||
/> | |||||
</fieldset> | |||||
) | |||||
DisabledWithinFieldset.args = { | |||||
label: 'Checkbox label', | |||||
disabled: true, | |||||
} | |||||
export default meta |
@@ -5,7 +5,7 @@ import * as fc from 'fast-check' | |||||
import * as Enzyme from 'enzyme' | import * as Enzyme from 'enzyme' | ||||
import * as Axe from 'jest-axe' | import * as Axe from 'jest-axe' | ||||
import * as React from 'react' | import * as React from 'react' | ||||
import Checkbox from './Checkbox' | |||||
import Checkbox from '.' | |||||
import stringify from '../../utilities/stringify' | import stringify from '../../utilities/stringify' | ||||
it('should exist', () => { | it('should exist', () => { | ||||
@@ -1,18 +1,16 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import * as PropTypes from 'prop-types' | |||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
import stringify from '../../utilities/stringify' | |||||
import Icon from '../Icon/Icon' | |||||
const Base = styled('div')({ | |||||
display: 'block', | |||||
}) | |||||
import Icon from '../Icon' | |||||
const CaptureArea = styled('label')({ | const CaptureArea = styled('label')({ | ||||
marginTop: '0.25rem', | |||||
display: 'inline-block', | |||||
cursor: 'pointer', | |||||
'::after': { | '::after': { | ||||
content: '""', | content: '""', | ||||
}, | }, | ||||
'fieldset[disabled] &': { | |||||
cursor: 'not-allowed', | |||||
}, | |||||
}) | }) | ||||
CaptureArea.displayName = 'label' | CaptureArea.displayName = 'label' | ||||
@@ -37,16 +35,14 @@ const IndicatorWrapper = styled('span')({ | |||||
minWidth: '1.5rem', | minWidth: '1.5rem', | ||||
maxWidth: '1.5rem', | maxWidth: '1.5rem', | ||||
display: 'inline-flex', | display: 'inline-flex', | ||||
verticalAlign: 'top', | |||||
verticalAlign: 'middle', | |||||
justifyContent: 'center', | justifyContent: 'center', | ||||
alignItems: 'center', | alignItems: 'center', | ||||
cursor: 'pointer', | |||||
transitionProperty: 'border-color', | transitionProperty: 'border-color', | ||||
[`${Input}:focus ~ &`]: { | [`${Input}:focus ~ &`]: { | ||||
'--color-accent': 'var(--color-active, Highlight)', | '--color-accent': 'var(--color-active, Highlight)', | ||||
}, | }, | ||||
[`${Input}:disabled ~ &`]: { | [`${Input}:disabled ~ &`]: { | ||||
cursor: 'not-allowed', | |||||
opacity: 0.5, | opacity: 0.5, | ||||
}, | }, | ||||
}) | }) | ||||
@@ -75,7 +71,7 @@ const Border = styled('span')({ | |||||
opacity: 0.5, | opacity: 0.5, | ||||
pointerEvents: 'none', | pointerEvents: 'none', | ||||
}, | }, | ||||
[`${Base}:focus-within &::before`]: { | |||||
[`${CaptureArea}:focus-within &::before`]: { | |||||
boxShadow: '0 0 0 0.375rem var(--color-accent, blue)', | boxShadow: '0 0 0 0.375rem var(--color-accent, blue)', | ||||
}, | }, | ||||
}) | }) | ||||
@@ -101,87 +97,69 @@ const Indicator = styled('span')({ | |||||
}) | }) | ||||
const Label = styled('span')({ | const Label = styled('span')({ | ||||
display: 'block', | |||||
verticalAlign: 'top', | |||||
float: 'right', | |||||
color: 'var(--color-accent, blue)', | |||||
width: 'calc(100% - 2.5rem)', | width: 'calc(100% - 2.5rem)', | ||||
fontFamily: 'var(--font-family-base, sans-serif)', | fontFamily: 'var(--font-family-base, sans-serif)', | ||||
pointerEvents: 'none', | |||||
marginTop: '-0.25em', | |||||
}) | |||||
const LabelContent = styled('span')({ | |||||
display: 'inline', | |||||
pointerEvents: 'auto', | |||||
[`${Input}:focus ~ &`]: { | |||||
'--color-accent': 'var(--color-active, Highlight)', | |||||
}, | |||||
[`${Input}:disabled ~ &`]: { | |||||
opacity: 0.5, | |||||
}, | |||||
}) | }) | ||||
const propTypes = { | |||||
export type Props = Omit<React.HTMLProps<HTMLInputElement>, 'as' | 'className' | 'style'> & { | |||||
/** | /** | ||||
* Short textual description indicating the nature of the component's value. | * Short textual description indicating the nature of the component's value. | ||||
*/ | */ | ||||
label: PropTypes.any, | |||||
/** | |||||
* Name of the form field associated with this component. | |||||
*/ | |||||
name: PropTypes.string, | |||||
/** | |||||
* Event handler triggered when the component is toggled. | |||||
*/ | |||||
onChange: PropTypes.func, | |||||
/** | |||||
* Event handler triggered when the component receives focus. | |||||
*/ | |||||
onFocus: PropTypes.func, | |||||
label?: React.ReactNode, | |||||
/** | /** | ||||
* Event handler triggered when the component loses focus. | |||||
* Should the label not be visible? | |||||
*/ | */ | ||||
onBlur: PropTypes.func, | |||||
/** | |||||
* Value of the component. | |||||
*/ | |||||
value: PropTypes.any, | |||||
/** | |||||
* Checked state of the component. | |||||
*/ | |||||
checked: PropTypes.bool, | |||||
hiddenLabel?: boolean, | |||||
} | } | ||||
type Props = PropTypes.InferProps<typeof propTypes> | |||||
/** | /** | ||||
* Component for values that have an on/off state. | * Component for values that have an on/off state. | ||||
* @see {@link Select} for a similar component suitable for selecting more values. | * @see {@link Select} for a similar component suitable for selecting more values. | ||||
* @see {@link RadioButton} for a similar component on selecting a single value among very few choices. | * @see {@link RadioButton} for a similar component on selecting a single value among very few choices. | ||||
*/ | */ | ||||
const Checkbox = React.forwardRef<HTMLInputElement, Props>(({ label = '', name, onChange, onFocus, onBlur, value, checked, }, ref) => ( | |||||
<Base> | |||||
<CaptureArea> | |||||
<Input | |||||
ref={ref} | |||||
type="checkbox" | |||||
name={name!} | |||||
onChange={onChange as React.ChangeEventHandler} | |||||
onFocus={onFocus as React.FocusEventHandler} | |||||
onBlur={onBlur as React.FocusEventHandler} | |||||
value={value} | |||||
checked={checked} | |||||
/> | |||||
<IndicatorWrapper> | |||||
<Border /> | |||||
<Indicator> | |||||
<Icon name="check" label="" /> | |||||
</Indicator> | |||||
</IndicatorWrapper> | |||||
{typeof label! !== 'undefined' && label !== null && ' '} | |||||
<Label> | |||||
<LabelContent>{stringify(label)}</LabelContent> | |||||
</Label> | |||||
</CaptureArea> | |||||
</Base> | |||||
export const Checkbox = React.forwardRef<HTMLInputElement, Props>(({ | |||||
label = '', | |||||
disabled = false, | |||||
hiddenLabel = false, | |||||
...etcProps | |||||
}, ref) => ( | |||||
<CaptureArea | |||||
style={{ | |||||
cursor: disabled ? 'not-allowed' : undefined, | |||||
}} | |||||
> | |||||
<Input | |||||
{...etcProps} | |||||
ref={ref} | |||||
type="checkbox" | |||||
aria-label={label} | |||||
disabled={disabled} | |||||
/> | |||||
<IndicatorWrapper> | |||||
<Border /> | |||||
<Indicator> | |||||
<Icon name="check" label="" /> | |||||
</Indicator> | |||||
</IndicatorWrapper> | |||||
{ | |||||
typeof label! !== 'undefined' | |||||
&& label !== null | |||||
&& !hiddenLabel | |||||
&& ( | |||||
<> | |||||
{' '} | |||||
<Label>{label}</Label> | |||||
</> | |||||
) | |||||
} | |||||
</CaptureArea> | |||||
)) | )) | ||||
Checkbox.propTypes = propTypes | |||||
Checkbox.displayName = 'Checkbox' | Checkbox.displayName = 'Checkbox' | ||||
export default Checkbox |
@@ -0,0 +1,42 @@ | |||||
import * as React from 'react' | |||||
import { Icon, Props } from '.' | |||||
import { Meta, Story } from '@storybook/react' | |||||
import { | |||||
Title, | |||||
Subtitle, | |||||
Description, | |||||
Primary, | |||||
ArgsTable, | |||||
Stories, | |||||
PRIMARY_STORY, | |||||
} from '@storybook/addon-docs' | |||||
export default { | |||||
title: 'components/Icon', | |||||
component: Icon, | |||||
parameters: { | |||||
docs: { | |||||
page: () => ( | |||||
<> | |||||
<Title /> | |||||
<Subtitle /> | |||||
<Description /> | |||||
<Primary /> | |||||
<ArgsTable story={PRIMARY_STORY} /> | |||||
<Stories /> | |||||
</> | |||||
), | |||||
}, | |||||
}, | |||||
} as Meta | |||||
const Template: Story<Props> = ({ ...args }) => ( | |||||
<Icon | |||||
{...args} | |||||
/> | |||||
) | |||||
export const Default = Template.bind({}) | |||||
Default.args = { | |||||
name: 'heart', | |||||
} |
@@ -6,7 +6,7 @@ import * as Enzyme from 'enzyme' | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import * as FeatherIcon from 'react-feather' | import * as FeatherIcon from 'react-feather' | ||||
import paramCase from 'param-case' | import paramCase from 'param-case' | ||||
import Icon from './Icon' | |||||
import Icon from '.' | |||||
const FEATHER_ICONS = Object.keys(FeatherIcon).map((k) => paramCase(k)) | const FEATHER_ICONS = Object.keys(FeatherIcon).map((k) => paramCase(k)) | ||||
@@ -1,5 +1,4 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import * as PropTypes from 'prop-types' | |||||
import * as FeatherIcon from 'react-feather' | import * as FeatherIcon from 'react-feather' | ||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
import { pascalCase, pascalCaseTransformMerge } from 'pascal-case' | import { pascalCase, pascalCaseTransformMerge } from 'pascal-case' | ||||
@@ -22,36 +21,31 @@ const StyledIcon = styled('svg')({ | |||||
verticalAlign: 'middle', | verticalAlign: 'middle', | ||||
}) | }) | ||||
const propTypes = { | |||||
export type Props = { | |||||
/** | /** | ||||
* Name of the icon to display. | * Name of the icon to display. | ||||
*/ | */ | ||||
name: PropTypes.string.isRequired, | |||||
name: string, | |||||
/** | /** | ||||
* Width of the icon's strokes. | * Width of the icon's strokes. | ||||
*/ | */ | ||||
weight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||||
weight?: string | number, | |||||
/** | /** | ||||
* Size of the icon. This controls both the width and the height. | * Size of the icon. This controls both the width and the height. | ||||
*/ | */ | ||||
size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||||
size?: string | number, | |||||
/** | /** | ||||
* Description of what the component represents. | * Description of what the component represents. | ||||
*/ | */ | ||||
label: PropTypes.string, | |||||
label?: string, | |||||
} | } | ||||
type Props = PropTypes.InferProps<typeof propTypes> | |||||
/** | /** | ||||
* Component for displaying graphics. | |||||
* @param name - Name of the icon to display. | |||||
* @param weight - Width of the icon's strokes. | |||||
* @param size - Size of the icon. This controls both the width and the height. | |||||
* @param label - Description of what the component represents. | |||||
* Component for displaying simple graphics. | |||||
* | |||||
* @see {@link https://feathericons.com|Feather Icons} for a list of icon names. | * @see {@link https://feathericons.com|Feather Icons} for a list of icon names. | ||||
*/ | */ | ||||
const Icon: React.FC<Props> = ({ name, weight = '0.125rem', size = '1.5rem', label = name }) => { | |||||
export const Icon: React.FC<Props> = ({ name, weight = '0.125rem', size = '1.5rem', label = name }) => { | |||||
const iconName = pascalCase(name, { transform: pascalCaseTransformMerge }) | const iconName = pascalCase(name, { transform: pascalCaseTransformMerge }) | ||||
const { [iconName as keyof typeof FeatherIcon]: TheIcon = null } = FeatherIcon | const { [iconName as keyof typeof FeatherIcon]: TheIcon = null } = FeatherIcon | ||||
const { magnitude: sizeValue, unit: sizeUnit } = splitValueAndUnit(size) | const { magnitude: sizeValue, unit: sizeUnit } = splitValueAndUnit(size) | ||||
@@ -82,8 +76,6 @@ const Icon: React.FC<Props> = ({ name, weight = '0.125rem', size = '1.5rem', lab | |||||
return null | return null | ||||
} | } | ||||
Icon.propTypes = propTypes | |||||
Icon.displayName = 'Icon' | Icon.displayName = 'Icon' | ||||
export default Icon | export default Icon |
@@ -0,0 +1,136 @@ | |||||
/// <reference types="jest-enzyme" /> | |||||
/// <reference path="../../../../../utilities/jest/extensions.ts" /> | |||||
import * as fc from 'fast-check' | |||||
import * as Enzyme from 'enzyme' | |||||
import * as Axe from 'jest-axe' | |||||
import * as React from 'react' | |||||
import MultilineTextInput from './index' | |||||
import stringify from '../../utilities/stringify' | |||||
it('should exist', () => { | |||||
expect(MultilineTextInput).toBeDefined() | |||||
}) | |||||
it('should be a component', () => { | |||||
expect(MultilineTextInput).toBeComponent() | |||||
}) | |||||
it('should render without crashing given required props', () => { | |||||
expect(() => <MultilineTextInput />).not.toThrow() | |||||
}) | |||||
it('should render a base element to put interactive elements on', () => { | |||||
const wrapper = Enzyme.shallow(<MultilineTextInput />) | |||||
expect(wrapper.find('label')).toHaveLength(1) | |||||
}) | |||||
it('should render an indicator as additional description for the content', () => { | |||||
fc.assert( | |||||
fc.property(fc.string(1, 20), (indicator) => { | |||||
const wrapper = Enzyme.shallow(<MultilineTextInput indicator={indicator} />) | |||||
expect(wrapper.find('div').childAt(1)).toHaveText(indicator) | |||||
}), | |||||
) | |||||
}) | |||||
it('should render a hint for describing valid input values', () => { | |||||
fc.assert( | |||||
fc.property(fc.anything(), (label) => { | |||||
const wrapper = Enzyme.shallow(<MultilineTextInput hint={label} />) | |||||
const renderedLabel = stringify(label) | |||||
if (renderedLabel.length > 0) { | |||||
expect(wrapper.find('div').children()).toHaveLength(3) | |||||
expect(wrapper.find('div').childAt(2)).toHaveText(`(${renderedLabel})`) | |||||
} else { | |||||
expect(wrapper.find('div').children()).toHaveLength(1) | |||||
} | |||||
}), | |||||
) | |||||
}) | |||||
it('should render a padding on the hint element when an indicator is present', () => { | |||||
const wrapper = Enzyme.shallow(<MultilineTextInput hint="foo" indicator="foobar" />) | |||||
expect(wrapper.find('div').childAt(3)).not.toHaveStyle('padding', '1rem') | |||||
}) | |||||
it('should render as half-opaque when disabled', () => { | |||||
const wrapper = Enzyme.shallow(<MultilineTextInput disabled />) | |||||
expect(wrapper.find('div')).toHaveStyle('opacity', 0.5) | |||||
}) | |||||
describe.each` | |||||
multiline | tag | |||||
${false} | ${'input'} | |||||
${true} | ${'textarea'} | |||||
`('on multiline (multiline=$multiline)', ({ tag: rawTag, multiline: rawMultiline }) => { | |||||
const tag = rawTag as string | |||||
const multiline = rawMultiline as boolean | |||||
it('should render an element to input text on', () => { | |||||
const wrapper = Enzyme.shallow(<MultilineTextInput multiline={multiline} />) | |||||
expect(wrapper.find('label').find(tag)).toHaveLength(1) | |||||
}) | |||||
it('should render the rest of the passed props', () => { | |||||
const wrapper = Enzyme.shallow(<MultilineTextInput multiline={multiline} placeholder="foo" />) | |||||
expect( | |||||
wrapper | |||||
.find('label') | |||||
.find(tag) | |||||
.prop('placeholder'), | |||||
).toBe('foo') | |||||
}) | |||||
it('should render a padding on the input element when an indicator is present', () => { | |||||
const wrapper = Enzyme.shallow(<MultilineTextInput multiline={multiline} indicator="foobar" />) | |||||
expect( | |||||
wrapper | |||||
.find('div') | |||||
.find(tag) | |||||
.prop('style')!.paddingRight, | |||||
).not.toBe('1rem') | |||||
}) | |||||
}) | |||||
describe('on aiding user input', () => { | |||||
it("should render a label to indicate the nature of the component's value", () => { | |||||
const wrapper = Enzyme.shallow(<MultilineTextInput label="foo" />) | |||||
expect(wrapper.find('label').find('span')).toHaveLength(1) | |||||
}) | |||||
it('should render the label text', () => { | |||||
fc.assert( | |||||
fc.property(fc.anything(), (label) => { | |||||
const wrapper = Enzyme.shallow(<MultilineTextInput label={label} />) | |||||
expect(wrapper.find('label').find('span')).toHaveText(stringify(label)) | |||||
}), | |||||
{ | |||||
numRuns: 300, | |||||
}, | |||||
) | |||||
}) | |||||
}) | |||||
it('should guarantee minimal accessibility', () => { | |||||
fc.assert( | |||||
fc.asyncProperty(fc.string(1, 20), async (s) => { | |||||
const wrapper = Enzyme.mount(<MultilineTextInput label={s} />) | |||||
const results = await Axe.axe(wrapper.getDOMNode()) | |||||
expect(results).toHaveNoViolations() | |||||
}), | |||||
) | |||||
}) |
@@ -2,38 +2,44 @@ import * as React from 'react' | |||||
import * as PropTypes from 'prop-types' | import * as PropTypes from 'prop-types' | ||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
import stringify from '../../utilities/stringify' | import stringify from '../../utilities/stringify' | ||||
import { Size, SizeMap } from '../../utilities/utilities' | |||||
import { Size } from '../../utilities/utilities' | |||||
// TODO implement web-client text inputs! | // TODO implement web-client text inputs! | ||||
const MIN_HEIGHTS: SizeMap<string | number> = { | |||||
small: '2.5rem', | |||||
medium: '3rem', | |||||
large: '4rem', | |||||
export enum MultilineTextInputSize { | |||||
SMALL = 'small', | |||||
MEDIUM = 'medium', | |||||
LARGE = 'large', | |||||
} | } | ||||
const LABEL_VERTICAL_PADDING_SIZES: SizeMap<string | number> = { | |||||
small: '0.125rem', | |||||
medium: '0.25rem', | |||||
large: '0.5rem', | |||||
const MIN_HEIGHTS: Record<MultilineTextInputSize, string | number> = { | |||||
[MultilineTextInputSize.SMALL]: '2.5rem', | |||||
[MultilineTextInputSize.MEDIUM]: '3rem', | |||||
[MultilineTextInputSize.LARGE]: '4rem', | |||||
} | } | ||||
const VERTICAL_PADDING_SIZES: SizeMap<string | number> = { | |||||
small: '0.6rem', | |||||
medium: '0.85rem', | |||||
large: '1.25rem', | |||||
const LABEL_VERTICAL_PADDING_SIZES: Record<MultilineTextInputSize, string | number> = { | |||||
[MultilineTextInputSize.SMALL]: '0.125rem', | |||||
[MultilineTextInputSize.MEDIUM]: '0.25rem', | |||||
[MultilineTextInputSize.LARGE]: '0.5rem', | |||||
} | } | ||||
const INPUT_FONT_SIZES: SizeMap<string | number> = { | |||||
small: '0.85em', | |||||
medium: '0.85em', | |||||
large: '1em', | |||||
const VERTICAL_PADDING_SIZES: Record<MultilineTextInputSize, string | number> = { | |||||
[MultilineTextInputSize.SMALL]: '0.6rem', | |||||
[MultilineTextInputSize.MEDIUM]: '0.85rem', | |||||
[MultilineTextInputSize.LARGE]: '1.25rem', | |||||
} | } | ||||
const SECONDARY_TEXT_SIZES: SizeMap<string | number> = { | |||||
small: '0.65em', | |||||
medium: '0.75em', | |||||
large: '0.85em', | |||||
const INPUT_FONT_SIZES: Record<MultilineTextInputSize, string | number> = { | |||||
[MultilineTextInputSize.SMALL]: '0.85em', | |||||
[MultilineTextInputSize.MEDIUM]: '0.85em', | |||||
[MultilineTextInputSize.LARGE]: '1em', | |||||
} | |||||
const SECONDARY_TEXT_SIZES: Record<MultilineTextInputSize, string | number> = { | |||||
[MultilineTextInputSize.SMALL]: '0.65em', | |||||
[MultilineTextInputSize.MEDIUM]: '0.75em', | |||||
[MultilineTextInputSize.LARGE]: '0.85em', | |||||
} | } | ||||
const ComponentBase = styled('div')({ | const ComponentBase = styled('div')({ | ||||
@@ -269,21 +275,27 @@ const propTypes = { | |||||
name: PropTypes.string, | name: PropTypes.string, | ||||
} | } | ||||
type Props = PropTypes.InferProps<typeof propTypes> | |||||
type Props = { | |||||
label?: React.ReactNode, | |||||
hint?: React.ReactNode, | |||||
size?: MultilineTextInputSize, | |||||
indicator?: React.ReactNode, | |||||
border?: boolean, | |||||
alternate?: boolean, | |||||
} | |||||
/** | /** | ||||
* Component for inputting textual values. | * Component for inputting textual values. | ||||
* | * | ||||
* This component supports multiline input and adjusts its layout accordingly. | * This component supports multiline input and adjusts its layout accordingly. | ||||
*/ | */ | ||||
const TextInput = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, Props>( | |||||
const MultilineTextInput = React.forwardRef<HTMLTextAreaElement, Props>( | |||||
( | ( | ||||
{ | { | ||||
label = '', | label = '', | ||||
hint = '', | hint = '', | ||||
indicator = null, | indicator = null, | ||||
size = 'medium', | |||||
multiline = false, | |||||
size = MultilineTextInputSize.MEDIUM, | |||||
disabled = false, | disabled = false, | ||||
autoResize = false, | autoResize = false, | ||||
placeholder = '', | placeholder = '', | ||||
@@ -318,49 +330,27 @@ const TextInput = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, Props | |||||
{stringify(label)} | {stringify(label)} | ||||
</LabelWrapper> | </LabelWrapper> | ||||
{stringify(label).length > 0 && ' '} | {stringify(label).length > 0 && ' '} | ||||
{multiline && ( | |||||
<TextArea | |||||
onChange={onChange as React.ChangeEventHandler} | |||||
onFocus={onFocus as React.FocusEventHandler} | |||||
onBlur={onBlur as React.FocusEventHandler} | |||||
placeholder={placeholder!} | |||||
ref={ref as React.Ref<HTMLTextAreaElement>} | |||||
disabled={disabled!} | |||||
rows={rows!} | |||||
style={{ | |||||
height: `calc(${MIN_HEIGHTS[size!]} * ${rows})`, | |||||
fontSize: INPUT_FONT_SIZES[size!], | |||||
minHeight: MIN_HEIGHTS[size!], | |||||
paddingTop: alternate ? VERTICAL_PADDING_SIZES[size!] : `calc(${SECONDARY_TEXT_SIZES[size!]} * 2)`, | |||||
paddingBottom: VERTICAL_PADDING_SIZES[size!], | |||||
paddingRight: indicator ? MIN_HEIGHTS[size!] : (alternate ? '1rem' : undefined), | |||||
paddingLeft: alternate ? '1rem' : undefined, | |||||
}} | |||||
defaultValue={defaultValue} | |||||
value={value} | |||||
name={name} | |||||
/> | |||||
)} | |||||
{!multiline && ( | |||||
<Input | |||||
onChange={onChange as React.ChangeEventHandler} | |||||
onFocus={onFocus as React.FocusEventHandler} | |||||
onBlur={onBlur as React.FocusEventHandler} | |||||
placeholder={placeholder!} | |||||
ref={ref as React.Ref<HTMLInputElement>} | |||||
disabled={disabled!} | |||||
style={{ | |||||
paddingLeft: alternate ? '1rem' : undefined, | |||||
fontSize: INPUT_FONT_SIZES[size!], | |||||
minHeight: MIN_HEIGHTS[size!], | |||||
paddingTop: alternate ? undefined : `calc(${SECONDARY_TEXT_SIZES[size!]} * 2)`, | |||||
paddingRight: indicator ? MIN_HEIGHTS[size!] : (alternate ? '1rem' : undefined), | |||||
}} | |||||
defaultValue={defaultValue} | |||||
value={value} | |||||
name={name} | |||||
/> | |||||
)} | |||||
<TextArea | |||||
onChange={onChange as React.ChangeEventHandler} | |||||
onFocus={onFocus as React.FocusEventHandler} | |||||
onBlur={onBlur as React.FocusEventHandler} | |||||
placeholder={placeholder!} | |||||
ref={ref as React.Ref<HTMLTextAreaElement>} | |||||
disabled={disabled!} | |||||
rows={rows!} | |||||
style={{ | |||||
height: `calc(${MIN_HEIGHTS[size!]} * ${rows})`, | |||||
fontSize: INPUT_FONT_SIZES[size!], | |||||
minHeight: MIN_HEIGHTS[size!], | |||||
paddingTop: alternate ? VERTICAL_PADDING_SIZES[size!] : `calc(${SECONDARY_TEXT_SIZES[size!]} * 2)`, | |||||
paddingBottom: VERTICAL_PADDING_SIZES[size!], | |||||
paddingRight: indicator ? MIN_HEIGHTS[size!] : (alternate ? '1rem' : undefined), | |||||
paddingLeft: alternate ? '1rem' : undefined, | |||||
}} | |||||
defaultValue={defaultValue} | |||||
value={value} | |||||
name={name} | |||||
/> | |||||
</CaptureArea> | </CaptureArea> | ||||
{stringify(hint).length > 0 && ' '} | {stringify(hint).length > 0 && ' '} | ||||
{stringify(hint).length > 0 && ( | {stringify(hint).length > 0 && ( | ||||
@@ -392,8 +382,8 @@ const TextInput = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, Props | |||||
), | ), | ||||
) | ) | ||||
TextInput.propTypes = propTypes | |||||
MultilineTextInput.propTypes = propTypes | |||||
TextInput.displayName = 'TextInput' | |||||
MultilineTextInput.displayName = 'TextInput' | |||||
export default TextInput | |||||
export default MultilineTextInput |
@@ -0,0 +1,74 @@ | |||||
import * as React from 'react' | |||||
import { RadioButton, Props } from '.' | |||||
import { Meta, Story } from '@storybook/react' | |||||
const meta: Meta = { | |||||
title: 'components/RadioButton', | |||||
component: RadioButton, | |||||
argTypes: { onChange: { action: 'changed' } }, | |||||
} | |||||
const Template: Story<Props> = ({ ref, label, ...args }) => { | |||||
return ( | |||||
<> | |||||
Some text | |||||
{' '} | |||||
<RadioButton | |||||
{...args} | |||||
label={label + '1'} | |||||
/> | |||||
{' '} | |||||
Another text | |||||
{' '} | |||||
<RadioButton | |||||
{...args} | |||||
label={label + '2'} | |||||
/> | |||||
{' '} | |||||
Some other text | |||||
{' '} | |||||
<RadioButton | |||||
{...args} | |||||
label={label + '3'} | |||||
/> | |||||
</> | |||||
) | |||||
} | |||||
export const Default = Template.bind({}) | |||||
Default.args = { | |||||
label: 'RadioButton label', | |||||
name: 'default', | |||||
} | |||||
export const Disabled = Template.bind({}) | |||||
Disabled.args = { | |||||
label: 'RadioButton label', | |||||
name: 'disabled', | |||||
disabled: true, | |||||
} | |||||
export const HiddenLabel = Template.bind({}) | |||||
HiddenLabel.args = { | |||||
label: 'RadioButton label', | |||||
name: 'hiddenLabel', | |||||
hiddenLabel: true, | |||||
} | |||||
export const DisabledWithinFieldset: Story<Props> = ({ ref, disabled, ...args }) => ( | |||||
<fieldset | |||||
disabled={disabled} | |||||
> | |||||
<RadioButton | |||||
{...args} | |||||
/> | |||||
</fieldset> | |||||
) | |||||
DisabledWithinFieldset.args = { | |||||
label: 'RadioButton label', | |||||
name: 'disabledWithinFieldset', | |||||
disabled: true, | |||||
} | |||||
export default meta |
@@ -5,7 +5,7 @@ import * as fc from 'fast-check' | |||||
import * as Enzyme from 'enzyme' | import * as Enzyme from 'enzyme' | ||||
import * as Axe from 'jest-axe' | import * as Axe from 'jest-axe' | ||||
import * as React from 'react' | import * as React from 'react' | ||||
import RadioButton from './RadioButton' | |||||
import RadioButton from '.' | |||||
import stringify from '../../utilities/stringify' | import stringify from '../../utilities/stringify' | ||||
it('should exist', () => { | it('should exist', () => { | ||||
@@ -1,17 +1,14 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import * as PropTypes from 'prop-types' | |||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
import stringify from '../../utilities/stringify' | |||||
const Base = styled('div')({ | |||||
display: 'block', | |||||
}) | |||||
const CaptureArea = styled('label')({ | const CaptureArea = styled('label')({ | ||||
marginTop: '0.25rem', | |||||
display: 'inline-block', | |||||
cursor: 'pointer', | |||||
'fieldset[disabled] &': { | |||||
cursor: 'not-allowed', | |||||
}, | |||||
'::after': { | '::after': { | ||||
content: '""', | content: '""', | ||||
clear: 'both', | |||||
}, | }, | ||||
}) | }) | ||||
@@ -46,7 +43,6 @@ const IndicatorWrapper = styled('span')({ | |||||
'--color-accent': 'var(--color-active, Highlight)', | '--color-accent': 'var(--color-active, Highlight)', | ||||
}, | }, | ||||
[`${Input}:disabled ~ &`]: { | [`${Input}:disabled ~ &`]: { | ||||
cursor: 'not-allowed', | |||||
opacity: 0.5, | opacity: 0.5, | ||||
}, | }, | ||||
}) | }) | ||||
@@ -75,7 +71,7 @@ const Border = styled('span')({ | |||||
opacity: 0.5, | opacity: 0.5, | ||||
pointerEvents: 'none', | pointerEvents: 'none', | ||||
}, | }, | ||||
[`${Base}:focus-within &::before`]: { | |||||
[`${CaptureArea}:focus-within &::before`]: { | |||||
boxShadow: '0 0 0 0.375rem var(--color-accent, blue)', | boxShadow: '0 0 0 0.375rem var(--color-accent, blue)', | ||||
}, | }, | ||||
}) | }) | ||||
@@ -99,88 +95,68 @@ const Indicator = styled('span')({ | |||||
}) | }) | ||||
const Label = styled('span')({ | const Label = styled('span')({ | ||||
display: 'block', | |||||
verticalAlign: 'top', | |||||
float: 'right', | |||||
color: 'var(--color-accent, blue)', | |||||
width: 'calc(100% - 2.5rem)', | width: 'calc(100% - 2.5rem)', | ||||
fontFamily: 'var(--font-family-base, sans-serif)', | fontFamily: 'var(--font-family-base, sans-serif)', | ||||
pointerEvents: 'none', | |||||
marginTop: '-0.25em', | |||||
}) | |||||
const LabelContent = styled('span')({ | |||||
display: 'inline', | |||||
pointerEvents: 'auto', | |||||
[`${Input}:focus ~ &`]: { | |||||
'--color-accent': 'var(--color-active, Highlight)', | |||||
}, | |||||
[`${Input}:disabled ~ &`]: { | |||||
opacity: 0.5, | |||||
}, | |||||
}) | }) | ||||
const propTypes = { | |||||
/** | |||||
* Group where the component belongs. | |||||
*/ | |||||
name: PropTypes.string.isRequired, | |||||
export type Props = Omit<React.HTMLProps<HTMLInputElement>, 'as' | 'className' | 'style'> & { | |||||
/** | /** | ||||
* Short textual description indicating the nature of the component's value. | * Short textual description indicating the nature of the component's value. | ||||
*/ | */ | ||||
label: PropTypes.any, | |||||
/** | |||||
* Event handler triggered when the component is selected. | |||||
*/ | |||||
onChange: PropTypes.func, | |||||
/** | |||||
* Event handler triggered when the component receives focus. | |||||
*/ | |||||
onFocus: PropTypes.func, | |||||
label?: React.ReactNode, | |||||
/** | /** | ||||
* Event handler triggered when the component loses focus. | |||||
* Should the label not be visible? | |||||
*/ | */ | ||||
onBlur: PropTypes.func, | |||||
/** | |||||
* Value of the component. | |||||
*/ | |||||
value: PropTypes.any, | |||||
/** | |||||
* Checked state of the component. | |||||
*/ | |||||
checked: PropTypes.bool, | |||||
hiddenLabel?: boolean, | |||||
} | } | ||||
type Props = PropTypes.InferProps<typeof propTypes> | |||||
/** | /** | ||||
* Component for values which are to be selected from a few list of options. | * Component for values which are to be selected from a few list of options. | ||||
* @see {@link Checkbox} for a similar component on selecting values among very few choices. | * @see {@link Checkbox} for a similar component on selecting values among very few choices. | ||||
* @see {@link Select} for a similar component suitable for selecting more values. | * @see {@link Select} for a similar component suitable for selecting more values. | ||||
* @type {React.ComponentType<{readonly label?: string, readonly name?: string} & React.ClassAttributes<unknown>>} | * @type {React.ComponentType<{readonly label?: string, readonly name?: string} & React.ClassAttributes<unknown>>} | ||||
*/ | */ | ||||
const RadioButton = React.forwardRef<HTMLInputElement, Props>( | |||||
({ label = '', name, onChange, onFocus, onBlur, value, checked, }, ref) => ( | |||||
<Base> | |||||
<CaptureArea> | |||||
<Input | |||||
ref={ref} | |||||
name={name} | |||||
type="radio" | |||||
onChange={onChange as React.ChangeEventHandler} | |||||
onFocus={onFocus as React.FocusEventHandler} | |||||
onBlur={onBlur as React.FocusEventHandler} | |||||
value={value} | |||||
checked={checked} | |||||
/> | |||||
<IndicatorWrapper> | |||||
<Border /> | |||||
<Indicator /> | |||||
</IndicatorWrapper> | |||||
{typeof label! !== 'undefined' && label !== null && ' '} | |||||
<Label> | |||||
<LabelContent>{stringify(label)}</LabelContent> | |||||
</Label> | |||||
</CaptureArea> | |||||
</Base> | |||||
), | |||||
) | |||||
RadioButton.propTypes = propTypes | |||||
export const RadioButton = React.forwardRef<HTMLInputElement, Props>(({ | |||||
label = '', | |||||
disabled = false, | |||||
hiddenLabel = false, | |||||
...etcProps | |||||
}, ref) => ( | |||||
<CaptureArea | |||||
style={{ | |||||
cursor: disabled ? 'not-allowed' : undefined, | |||||
}} | |||||
> | |||||
<Input | |||||
{...etcProps} | |||||
ref={ref} | |||||
type="radio" | |||||
aria-label={label} | |||||
disabled={disabled} | |||||
/> | |||||
<IndicatorWrapper> | |||||
<Border /> | |||||
<Indicator /> | |||||
</IndicatorWrapper> | |||||
{ | |||||
typeof label! !== 'undefined' | |||||
&& label !== null | |||||
&& !hiddenLabel | |||||
&& ( | |||||
<> | |||||
{' '} | |||||
<Label>{label}</Label> | |||||
</> | |||||
) | |||||
} | |||||
</CaptureArea> | |||||
)) | |||||
RadioButton.displayName = 'RadioButton' | RadioButton.displayName = 'RadioButton' | ||||
export default RadioButton |
@@ -0,0 +1,181 @@ | |||||
import * as React from 'react' | |||||
import { Props, Select, SelectSize } from '.' | |||||
import { Meta, Story } from '@storybook/react' | |||||
export default { | |||||
title: 'components/Select', | |||||
component: Select, | |||||
argTypes: { onChange: { action: 'changed' } }, | |||||
} as Meta | |||||
const Template: Story<Props> = ({ ref, ...args }) => ( | |||||
<> | |||||
Text here | |||||
{' '} | |||||
<Select | |||||
{...args} | |||||
/> | |||||
{' '} | |||||
Other text here | |||||
</> | |||||
) | |||||
export const Default = Template.bind({}) | |||||
Default.args = { | |||||
border: true, | |||||
label: '', | |||||
hint: '', | |||||
size: SelectSize.MEDIUM, | |||||
children: ( | |||||
<> | |||||
<option>Option 1</option> | |||||
<option>Option 2</option> | |||||
<option>Option 3</option> | |||||
</> | |||||
) | |||||
} | |||||
export const WithLabel = Template.bind({}) | |||||
WithLabel.args = { | |||||
border: true, | |||||
label: 'Foo', | |||||
children: ( | |||||
<> | |||||
<option>Option 1</option> | |||||
<option>Option 2</option> | |||||
<option>Option 3</option> | |||||
</> | |||||
) | |||||
} | |||||
export const WithLabelAndHint = Template.bind({}) | |||||
WithLabelAndHint.args = { | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
children: ( | |||||
<> | |||||
<option>Option 1</option> | |||||
<option>Option 2</option> | |||||
<option>Option 3</option> | |||||
</> | |||||
) | |||||
} | |||||
export const WithLabelAndHintAlternate = Template.bind({}) | |||||
WithLabelAndHintAlternate.args = { | |||||
border: true, | |||||
alternate: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
children: ( | |||||
<> | |||||
<option>Option 1</option> | |||||
<option>Option 2</option> | |||||
<option>Option 3</option> | |||||
</> | |||||
) | |||||
} | |||||
export const WithLabelAndHintAlternateBorderless = Template.bind({}) | |||||
WithLabelAndHintAlternateBorderless.args = { | |||||
alternate: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
children: ( | |||||
<> | |||||
<option>Option 1</option> | |||||
<option>Option 2</option> | |||||
<option>Option 3</option> | |||||
</> | |||||
) | |||||
} | |||||
export const WithLabelAndHintBorderless = Template.bind({}) | |||||
WithLabelAndHintBorderless.args = { | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
children: ( | |||||
<> | |||||
<option>Option 1</option> | |||||
<option>Option 2</option> | |||||
<option>Option 3</option> | |||||
</> | |||||
) | |||||
} | |||||
export const Block = Template.bind({}) | |||||
Block.args = { | |||||
border: true, | |||||
block: true, | |||||
children: ( | |||||
<> | |||||
<option>Option 1</option> | |||||
<option>Option 2</option> | |||||
<option>Option 3</option> | |||||
</> | |||||
) | |||||
} | |||||
export const WithLabelAndHintAlternateSmall = Template.bind({}) | |||||
WithLabelAndHintAlternateSmall.args = { | |||||
alternate: true, | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
size: SelectSize.SMALL, | |||||
children: ( | |||||
<> | |||||
<option>Option 1</option> | |||||
<option>Option 2</option> | |||||
<option>Option 3</option> | |||||
</> | |||||
) | |||||
} | |||||
export const WithLabelAndHintAlternateLarge = Template.bind({}) | |||||
WithLabelAndHintAlternateLarge.args = { | |||||
alternate: true, | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
size: SelectSize.LARGE, | |||||
children: ( | |||||
<> | |||||
<option>Option 1</option> | |||||
<option>Option 2</option> | |||||
<option>Option 3</option> | |||||
</> | |||||
) | |||||
} | |||||
export const WithLabelAndHintSmall = Template.bind({}) | |||||
WithLabelAndHintSmall.args = { | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
size: SelectSize.SMALL, | |||||
children: ( | |||||
<> | |||||
<option>Option 1</option> | |||||
<option>Option 2</option> | |||||
<option>Option 3</option> | |||||
</> | |||||
) | |||||
} | |||||
export const WithLabelAndHintLarge = Template.bind({}) | |||||
WithLabelAndHintLarge.args = { | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
size: SelectSize.LARGE, | |||||
children: ( | |||||
<> | |||||
<option>Option 1</option> | |||||
<option>Option 2</option> | |||||
<option>Option 3</option> | |||||
</> | |||||
) | |||||
} |
@@ -5,7 +5,7 @@ import * as fc from 'fast-check' | |||||
import * as Enzyme from 'enzyme' | import * as Enzyme from 'enzyme' | ||||
import * as Axe from 'jest-axe' | import * as Axe from 'jest-axe' | ||||
import * as React from 'react' | import * as React from 'react' | ||||
import Select from './Select' | |||||
import Select from '.' | |||||
import stringify from '../../utilities/stringify' | import stringify from '../../utilities/stringify' | ||||
it('should exist', () => { | it('should exist', () => { | ||||
@@ -1,41 +1,45 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import * as PropTypes from 'prop-types' | |||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
import stringify from '../../utilities/stringify' | |||||
import { Size, SizeMap } from '../../utilities/utilities' | |||||
import Icon from '../Icon/Icon' | |||||
import Icon from '../Icon' | |||||
const MIN_HEIGHTS: SizeMap<string | number> = { | |||||
small: '2.5rem', | |||||
medium: '3rem', | |||||
large: '4rem', | |||||
export enum SelectSize { | |||||
SMALL = 'small', | |||||
MEDIUM = 'medium', | |||||
LARGE = 'large', | |||||
} | } | ||||
const LABEL_VERTICAL_PADDING_SIZES: SizeMap<string | number> = { | |||||
small: '0.125rem', | |||||
medium: '0.25rem', | |||||
large: '0.5rem', | |||||
const MIN_HEIGHTS: Record<SelectSize, string | number> = { | |||||
[SelectSize.SMALL]: '2.5rem', | |||||
[SelectSize.MEDIUM]: '3rem', | |||||
[SelectSize.LARGE]: '4rem', | |||||
} | } | ||||
const VERTICAL_PADDING_SIZES: SizeMap<string | number> = { | |||||
small: '0.6rem', | |||||
medium: '0.85rem', | |||||
large: '1.25rem', | |||||
const LABEL_VERTICAL_PADDING_SIZES: Record<SelectSize, string | number> = { | |||||
[SelectSize.SMALL]: '0.125rem', | |||||
[SelectSize.MEDIUM]: '0.25rem', | |||||
[SelectSize.LARGE]: '0.5rem', | |||||
} | } | ||||
const INPUT_FONT_SIZES: SizeMap<string | number> = { | |||||
small: '0.85em', | |||||
medium: '0.85em', | |||||
large: '1em', | |||||
const VERTICAL_PADDING_SIZES: Record<SelectSize, string | number> = { | |||||
[SelectSize.SMALL]: '0.6rem', | |||||
[SelectSize.MEDIUM]: '0.85rem', | |||||
[SelectSize.LARGE]: '1.25rem', | |||||
} | } | ||||
const SECONDARY_TEXT_SIZES: SizeMap<string | number> = { | |||||
small: '0.65em', | |||||
medium: '0.75em', | |||||
large: '0.85em', | |||||
const INPUT_FONT_SIZES: Record<SelectSize, string | number> = { | |||||
[SelectSize.SMALL]: '0.85em', | |||||
[SelectSize.MEDIUM]: '0.85em', | |||||
[SelectSize.LARGE]: '1em', | |||||
} | |||||
const SECONDARY_TEXT_SIZES: Record<SelectSize, string | number> = { | |||||
[SelectSize.SMALL]: '0.65em', | |||||
[SelectSize.MEDIUM]: '0.75em', | |||||
[SelectSize.LARGE]: '0.85em', | |||||
} | } | ||||
const ComponentBase = styled('div')({ | const ComponentBase = styled('div')({ | ||||
verticalAlign: 'middle', | |||||
position: 'relative', | position: 'relative', | ||||
borderRadius: '0.25rem', | borderRadius: '0.25rem', | ||||
fontFamily: 'var(--font-family-base, sans-serif)', | fontFamily: 'var(--font-family-base, sans-serif)', | ||||
@@ -43,12 +47,17 @@ const ComponentBase = styled('div')({ | |||||
':focus-within': { | ':focus-within': { | ||||
'--color-accent': 'var(--color-active, Highlight)', | '--color-accent': 'var(--color-active, Highlight)', | ||||
}, | }, | ||||
'fieldset[disabled] &': { | |||||
opacity: 0.5, | |||||
}, | |||||
}) | }) | ||||
ComponentBase.displayName = 'div' | ComponentBase.displayName = 'div' | ||||
const CaptureArea = styled('label')({ | const CaptureArea = styled('label')({ | ||||
display: 'block', | display: 'block', | ||||
borderRadius: 'inherit', | |||||
overflow: 'hidden', | |||||
}) | }) | ||||
CaptureArea.displayName = 'label' | CaptureArea.displayName = 'label' | ||||
@@ -175,110 +184,85 @@ const IndicatorWrapper = styled('span')({ | |||||
userSelect: 'none', | userSelect: 'none', | ||||
}) | }) | ||||
const propTypes = { | |||||
export type Props = Omit<React.HTMLProps<HTMLSelectElement>, 'as' | 'className' | 'style' | 'size' | 'rows'> & { | |||||
/** | /** | ||||
* Short textual description indicating the nature of the component's value. | * Short textual description indicating the nature of the component's value. | ||||
*/ | */ | ||||
label: PropTypes.any, | |||||
label?: React.ReactNode, | |||||
/** | /** | ||||
* Short textual description as guidelines for valid input values. | * Short textual description as guidelines for valid input values. | ||||
*/ | */ | ||||
hint: PropTypes.any, | |||||
hint?: React.ReactNode, | |||||
/** | /** | ||||
* Size of the component. | * Size of the component. | ||||
*/ | */ | ||||
size: PropTypes.oneOf<Size>(['small', 'medium', 'large']), | |||||
/** | |||||
* Can multiple values be selected? | |||||
*/ | |||||
multiple: PropTypes.bool, | |||||
/** | |||||
* Is the component active? | |||||
*/ | |||||
disabled: PropTypes.bool, | |||||
/** | |||||
* Name of the form field associated with this component. | |||||
*/ | |||||
name: PropTypes.string, | |||||
/** | |||||
* Does the button display a border? | |||||
*/ | |||||
border: PropTypes.bool, | |||||
size?: SelectSize, | |||||
/** | /** | ||||
* Event handler triggered when the component changes value. | |||||
* Number of rows to display. | |||||
*/ | */ | ||||
onChange: PropTypes.func, | |||||
rows?: number, | |||||
/** | /** | ||||
* Event handler triggered when the component receives focus. | |||||
* Does the component display a border? | |||||
*/ | */ | ||||
onFocus: PropTypes.func, | |||||
border: boolean, | |||||
/** | /** | ||||
* Event handler triggered when the component loses focus. | |||||
* Should the component be displayed with an alternate appearance? | |||||
*/ | */ | ||||
onBlur: PropTypes.func, | |||||
alternate?: boolean, | |||||
/** | /** | ||||
* Default value of the component. | |||||
* Should the component occupy the whole width of its parent? | |||||
*/ | */ | ||||
defaultValue: PropTypes.any, | |||||
/** | |||||
* Value of the component. | |||||
*/ | |||||
value: PropTypes.any, | |||||
block?: boolean, | |||||
} | } | ||||
type Props = PropTypes.InferProps<typeof propTypes> | |||||
/** | /** | ||||
* Component for selecting values from a larger number of options. | * Component for selecting values from a larger number of options. | ||||
*/ | */ | ||||
const Select = React.forwardRef<HTMLSelectElement, Props>( | |||||
export const Select = React.forwardRef<HTMLSelectElement, Props>( | |||||
( | ( | ||||
{ | { | ||||
label = '', | label = '', | ||||
hint = '', | hint = '', | ||||
size = 'medium', | |||||
size = SelectSize.MEDIUM, | |||||
rows, | |||||
border = false, | |||||
multiple = false, | multiple = false, | ||||
disabled = false, | disabled = false, | ||||
name, | |||||
children, | |||||
border = false, | |||||
onChange, | |||||
onFocus, | |||||
onBlur, | |||||
defaultValue, | |||||
value, | |||||
block = false, | |||||
...etcProps | |||||
}, | }, | ||||
ref, | ref, | ||||
) => { | ) => { | ||||
return ( | return ( | ||||
<ComponentBase | <ComponentBase | ||||
style={{ | style={{ | ||||
display: block ? 'block' : 'inline-block', | |||||
opacity: disabled ? 0.5 : undefined, | opacity: disabled ? 0.5 : undefined, | ||||
}} | }} | ||||
> | > | ||||
{border && <Border />} | {border && <Border />} | ||||
<CaptureArea> | <CaptureArea> | ||||
<LabelWrapper | |||||
style={{ | |||||
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingRight: !multiple ? MIN_HEIGHTS[size!] : '0.5rem', | |||||
fontSize: SECONDARY_TEXT_SIZES[size!], | |||||
}} | |||||
> | |||||
{stringify(label)} | |||||
</LabelWrapper> | |||||
{stringify(label).length > 0 && ' '} | |||||
{label && ( | |||||
<> | |||||
<LabelWrapper | |||||
style={{ | |||||
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingRight: !multiple ? MIN_HEIGHTS[size!] : '0.5rem', | |||||
fontSize: SECONDARY_TEXT_SIZES[size!], | |||||
}} | |||||
> | |||||
{label} | |||||
</LabelWrapper> | |||||
{' '} | |||||
</> | |||||
)} | |||||
<Input | <Input | ||||
defaultValue={defaultValue} | |||||
value={value} | |||||
onChange={onChange as React.ChangeEventHandler} | |||||
onFocus={onFocus as React.FocusEventHandler} | |||||
onBlur={onBlur as React.FocusEventHandler} | |||||
{...etcProps} | |||||
ref={ref} | ref={ref} | ||||
disabled={disabled!} | |||||
multiple={multiple!} | |||||
name={name!} | |||||
disabled={disabled} | |||||
multiple={multiple} | |||||
size={rows} | |||||
style={{ | style={{ | ||||
verticalAlign: 'top', | verticalAlign: 'top', | ||||
fontSize: INPUT_FONT_SIZES[size!], | fontSize: INPUT_FONT_SIZES[size!], | ||||
@@ -289,22 +273,23 @@ const Select = React.forwardRef<HTMLSelectElement, Props>( | |||||
paddingBottom: VERTICAL_PADDING_SIZES[size!], | paddingBottom: VERTICAL_PADDING_SIZES[size!], | ||||
paddingRight: !multiple ? MIN_HEIGHTS[size!] : '1rem', | paddingRight: !multiple ? MIN_HEIGHTS[size!] : '1rem', | ||||
}} | }} | ||||
> | |||||
{children} | |||||
</Input> | |||||
aria-label={label} | |||||
/> | |||||
</CaptureArea> | </CaptureArea> | ||||
{stringify(hint).length > 0 && ' '} | |||||
{stringify(hint).length > 0 && ( | |||||
<HintWrapper | |||||
style={{ | |||||
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingRight: MIN_HEIGHTS[size!], | |||||
fontSize: SECONDARY_TEXT_SIZES[size!], | |||||
}} | |||||
> | |||||
{stringify(hint)} | |||||
</HintWrapper> | |||||
{hint && ( | |||||
<> | |||||
{' '} | |||||
<HintWrapper | |||||
style={{ | |||||
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingRight: MIN_HEIGHTS[size!], | |||||
fontSize: SECONDARY_TEXT_SIZES[size!], | |||||
}} | |||||
> | |||||
{hint} | |||||
</HintWrapper> | |||||
</> | |||||
)} | )} | ||||
{!multiple && ( | {!multiple && ( | ||||
<IndicatorWrapper | <IndicatorWrapper | ||||
@@ -320,8 +305,4 @@ const Select = React.forwardRef<HTMLSelectElement, Props>( | |||||
}, | }, | ||||
) | ) | ||||
Select.propTypes = propTypes | |||||
Select.displayName = 'Select' | Select.displayName = 'Select' | ||||
export default Select |
@@ -4,7 +4,7 @@ | |||||
import * as fc from 'fast-check' | import * as fc from 'fast-check' | ||||
import * as Enzyme from 'enzyme' | import * as Enzyme from 'enzyme' | ||||
import * as React from 'react' | import * as React from 'react' | ||||
import Slider, { Orientation } from './Slider' | |||||
import Slider, { Orientation } from '.' | |||||
it('should exist', () => { | it('should exist', () => { | ||||
expect(Slider).toBeDefined() | expect(Slider).toBeDefined() | ||||
@@ -0,0 +1,125 @@ | |||||
import * as React from 'react' | |||||
import { Props, TextInput, TextInputSize } from '.' | |||||
import { Meta, Story } from '@storybook/react' | |||||
export default { | |||||
title: 'components/TextInput', | |||||
component: TextInput, | |||||
argTypes: { onChange: { action: 'changed' } }, | |||||
} as Meta | |||||
const Template: Story<Props> = ({ ref, ...args }) => ( | |||||
<> | |||||
Text here | |||||
{' '} | |||||
<TextInput | |||||
{...args} | |||||
/> | |||||
{' '} | |||||
Other text here | |||||
</> | |||||
) | |||||
export const Default = Template.bind({}) | |||||
Default.args = { | |||||
border: true, | |||||
label: '', | |||||
hint: '', | |||||
indicator: '', | |||||
size: TextInputSize.MEDIUM, | |||||
} | |||||
export const WithLabel = Template.bind({}) | |||||
WithLabel.args = { | |||||
border: true, | |||||
label: 'Foo', | |||||
} | |||||
export const WithLabelAndHint = Template.bind({}) | |||||
WithLabelAndHint.args = { | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
} | |||||
export const WithLabelAndHintAlternate = Template.bind({}) | |||||
WithLabelAndHintAlternate.args = { | |||||
border: true, | |||||
alternate: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
} | |||||
export const WithLabelAndHintAlternateBorderless = Template.bind({}) | |||||
WithLabelAndHintAlternateBorderless.args = { | |||||
alternate: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
} | |||||
export const WithLabelAndHintBorderless = Template.bind({}) | |||||
WithLabelAndHintBorderless.args = { | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
} | |||||
export const Block = Template.bind({}) | |||||
Block.args = { | |||||
border: true, | |||||
block: true, | |||||
} | |||||
export const WithLabelHintAndIndicator = Template.bind({}) | |||||
WithLabelHintAndIndicator.args = { | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
indicator: 'A', | |||||
} | |||||
export const WithLabelHintAndIndicatorAlternate = Template.bind({}) | |||||
WithLabelHintAndIndicatorAlternate.args = { | |||||
alternate: true, | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
indicator: 'A', | |||||
} | |||||
export const WithLabelHintAndIndicatorAlternateSmall = Template.bind({}) | |||||
WithLabelHintAndIndicatorAlternateSmall.args = { | |||||
alternate: true, | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
indicator: 'A', | |||||
size: TextInputSize.SMALL, | |||||
} | |||||
export const WithLabelHintAndIndicatorAlternateLarge = Template.bind({}) | |||||
WithLabelHintAndIndicatorAlternateLarge.args = { | |||||
alternate: true, | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
indicator: 'A', | |||||
size: TextInputSize.LARGE, | |||||
} | |||||
export const WithLabelHintAndIndicatorSmall = Template.bind({}) | |||||
WithLabelHintAndIndicatorSmall.args = { | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
indicator: 'A', | |||||
size: TextInputSize.SMALL, | |||||
} | |||||
export const WithLabelHintAndIndicatorLarge = Template.bind({}) | |||||
WithLabelHintAndIndicatorLarge.args = { | |||||
border: true, | |||||
label: 'Foo', | |||||
hint: '(example value)', | |||||
indicator: 'A', | |||||
size: TextInputSize.LARGE, | |||||
} |
@@ -5,23 +5,23 @@ import * as fc from 'fast-check' | |||||
import * as Enzyme from 'enzyme' | import * as Enzyme from 'enzyme' | ||||
import * as Axe from 'jest-axe' | import * as Axe from 'jest-axe' | ||||
import * as React from 'react' | import * as React from 'react' | ||||
import TextInput from './TextInput' | |||||
import Index from '.' | |||||
import stringify from '../../utilities/stringify' | import stringify from '../../utilities/stringify' | ||||
it('should exist', () => { | it('should exist', () => { | ||||
expect(TextInput).toBeDefined() | |||||
expect(Index).toBeDefined() | |||||
}) | }) | ||||
it('should be a component', () => { | it('should be a component', () => { | ||||
expect(TextInput).toBeComponent() | |||||
expect(Index).toBeComponent() | |||||
}) | }) | ||||
it('should render without crashing given required props', () => { | it('should render without crashing given required props', () => { | ||||
expect(() => <TextInput />).not.toThrow() | |||||
expect(() => <Index />).not.toThrow() | |||||
}) | }) | ||||
it('should render a base element to put interactive elements on', () => { | it('should render a base element to put interactive elements on', () => { | ||||
const wrapper = Enzyme.shallow(<TextInput />) | |||||
const wrapper = Enzyme.shallow(<Index />) | |||||
expect(wrapper.find('label')).toHaveLength(1) | expect(wrapper.find('label')).toHaveLength(1) | ||||
}) | }) | ||||
@@ -29,7 +29,7 @@ it('should render a base element to put interactive elements on', () => { | |||||
it('should render an indicator as additional description for the content', () => { | it('should render an indicator as additional description for the content', () => { | ||||
fc.assert( | fc.assert( | ||||
fc.property(fc.string(1, 20), (indicator) => { | fc.property(fc.string(1, 20), (indicator) => { | ||||
const wrapper = Enzyme.shallow(<TextInput indicator={indicator} />) | |||||
const wrapper = Enzyme.shallow(<Index indicator={indicator} />) | |||||
expect(wrapper.find('div').childAt(1)).toHaveText(indicator) | expect(wrapper.find('div').childAt(1)).toHaveText(indicator) | ||||
}), | }), | ||||
@@ -39,7 +39,7 @@ it('should render an indicator as additional description for the content', () => | |||||
it('should render a hint for describing valid input values', () => { | it('should render a hint for describing valid input values', () => { | ||||
fc.assert( | fc.assert( | ||||
fc.property(fc.anything(), (label) => { | fc.property(fc.anything(), (label) => { | ||||
const wrapper = Enzyme.shallow(<TextInput hint={label} />) | |||||
const wrapper = Enzyme.shallow(<Index hint={label} />) | |||||
const renderedLabel = stringify(label) | const renderedLabel = stringify(label) | ||||
@@ -55,13 +55,13 @@ it('should render a hint for describing valid input values', () => { | |||||
}) | }) | ||||
it('should render a padding on the hint element when an indicator is present', () => { | it('should render a padding on the hint element when an indicator is present', () => { | ||||
const wrapper = Enzyme.shallow(<TextInput hint="foo" indicator="foobar" />) | |||||
const wrapper = Enzyme.shallow(<Index hint="foo" indicator="foobar" />) | |||||
expect(wrapper.find('div').childAt(3)).not.toHaveStyle('padding', '1rem') | expect(wrapper.find('div').childAt(3)).not.toHaveStyle('padding', '1rem') | ||||
}) | }) | ||||
it('should render as half-opaque when disabled', () => { | it('should render as half-opaque when disabled', () => { | ||||
const wrapper = Enzyme.shallow(<TextInput disabled />) | |||||
const wrapper = Enzyme.shallow(<Index disabled />) | |||||
expect(wrapper.find('div')).toHaveStyle('opacity', 0.5) | expect(wrapper.find('div')).toHaveStyle('opacity', 0.5) | ||||
}) | }) | ||||
@@ -75,13 +75,13 @@ describe.each` | |||||
const multiline = rawMultiline as boolean | const multiline = rawMultiline as boolean | ||||
it('should render an element to input text on', () => { | it('should render an element to input text on', () => { | ||||
const wrapper = Enzyme.shallow(<TextInput multiline={multiline} />) | |||||
const wrapper = Enzyme.shallow(<Index multiline={multiline} />) | |||||
expect(wrapper.find('label').find(tag)).toHaveLength(1) | expect(wrapper.find('label').find(tag)).toHaveLength(1) | ||||
}) | }) | ||||
it('should render the rest of the passed props', () => { | it('should render the rest of the passed props', () => { | ||||
const wrapper = Enzyme.shallow(<TextInput multiline={multiline} placeholder="foo" />) | |||||
const wrapper = Enzyme.shallow(<Index multiline={multiline} placeholder="foo" />) | |||||
expect( | expect( | ||||
wrapper | wrapper | ||||
@@ -92,7 +92,7 @@ describe.each` | |||||
}) | }) | ||||
it('should render a padding on the input element when an indicator is present', () => { | it('should render a padding on the input element when an indicator is present', () => { | ||||
const wrapper = Enzyme.shallow(<TextInput multiline={multiline} indicator="foobar" />) | |||||
const wrapper = Enzyme.shallow(<Index multiline={multiline} indicator="foobar" />) | |||||
expect( | expect( | ||||
wrapper | wrapper | ||||
@@ -105,7 +105,7 @@ describe.each` | |||||
describe('on aiding user input', () => { | describe('on aiding user input', () => { | ||||
it("should render a label to indicate the nature of the component's value", () => { | it("should render a label to indicate the nature of the component's value", () => { | ||||
const wrapper = Enzyme.shallow(<TextInput label="foo" />) | |||||
const wrapper = Enzyme.shallow(<Index label="foo" />) | |||||
expect(wrapper.find('label').find('span')).toHaveLength(1) | expect(wrapper.find('label').find('span')).toHaveLength(1) | ||||
}) | }) | ||||
@@ -113,7 +113,7 @@ describe('on aiding user input', () => { | |||||
it('should render the label text', () => { | it('should render the label text', () => { | ||||
fc.assert( | fc.assert( | ||||
fc.property(fc.anything(), (label) => { | fc.property(fc.anything(), (label) => { | ||||
const wrapper = Enzyme.shallow(<TextInput label={label} />) | |||||
const wrapper = Enzyme.shallow(<Index label={label} />) | |||||
expect(wrapper.find('label').find('span')).toHaveText(stringify(label)) | expect(wrapper.find('label').find('span')).toHaveText(stringify(label)) | ||||
}), | }), | ||||
@@ -127,7 +127,7 @@ describe('on aiding user input', () => { | |||||
it('should guarantee minimal accessibility', () => { | it('should guarantee minimal accessibility', () => { | ||||
fc.assert( | fc.assert( | ||||
fc.asyncProperty(fc.string(1, 20), async (s) => { | fc.asyncProperty(fc.string(1, 20), async (s) => { | ||||
const wrapper = Enzyme.mount(<TextInput label={s} />) | |||||
const wrapper = Enzyme.mount(<Index label={s} />) | |||||
const results = await Axe.axe(wrapper.getDOMNode()) | const results = await Axe.axe(wrapper.getDOMNode()) | ||||
expect(results).toHaveNoViolations() | expect(results).toHaveNoViolations() | ||||
@@ -0,0 +1,322 @@ | |||||
import * as React from 'react' | |||||
import styled from 'styled-components' | |||||
// TODO implement web-client text inputs! | |||||
export enum TextInputSize { | |||||
SMALL = 'small', | |||||
MEDIUM = 'medium', | |||||
LARGE = 'large', | |||||
} | |||||
const MIN_HEIGHTS: Record<TextInputSize, string | number> = { | |||||
[TextInputSize.SMALL]: '2.5rem', | |||||
[TextInputSize.MEDIUM]: '3rem', | |||||
[TextInputSize.LARGE]: '4rem', | |||||
} | |||||
const LABEL_VERTICAL_PADDING_SIZES: Record<TextInputSize, string | number> = { | |||||
[TextInputSize.SMALL]: '0.125rem', | |||||
[TextInputSize.MEDIUM]: '0.25rem', | |||||
[TextInputSize.LARGE]: '0.5rem', | |||||
} | |||||
const INPUT_FONT_SIZES: Record<TextInputSize, string | number> = { | |||||
[TextInputSize.SMALL]: '0.85em', | |||||
[TextInputSize.MEDIUM]: '0.85em', | |||||
[TextInputSize.LARGE]: '1em', | |||||
} | |||||
const SECONDARY_TEXT_SIZES: Record<TextInputSize, string | number> = { | |||||
[TextInputSize.SMALL]: '0.65em', | |||||
[TextInputSize.MEDIUM]: '0.75em', | |||||
[TextInputSize.LARGE]: '0.85em', | |||||
} | |||||
const ComponentBase = styled('div')({ | |||||
verticalAlign: 'middle', | |||||
position: 'relative', | |||||
borderRadius: '0.25rem', | |||||
fontFamily: 'var(--font-family-base, sans-serif)', | |||||
maxWidth: '100%', | |||||
':focus-within': { | |||||
'--color-accent': 'var(--color-active, Highlight)', | |||||
}, | |||||
'fieldset[disabled] &': { | |||||
opacity: 0.5, | |||||
}, | |||||
}) | |||||
ComponentBase.displayName = 'div' | |||||
const CaptureArea = styled('label')({ | |||||
display: 'block', | |||||
borderRadius: 'inherit', | |||||
overflow: 'hidden', | |||||
}) | |||||
CaptureArea.displayName = 'label' | |||||
const LabelWrapper = styled('span')({ | |||||
color: 'var(--color-accent, blue)', | |||||
boxSizing: 'border-box', | |||||
position: 'absolute', | |||||
top: 0, | |||||
left: 0, | |||||
fontSize: '0.85em', | |||||
maxWidth: '100%', | |||||
overflow: 'hidden', | |||||
textOverflow: 'ellipsis', | |||||
whiteSpace: 'nowrap', | |||||
fontWeight: 'bolder', | |||||
zIndex: 2, | |||||
pointerEvents: 'none', | |||||
transitionProperty: 'color', | |||||
lineHeight: 1, | |||||
userSelect: 'none', | |||||
}) | |||||
LabelWrapper.displayName = 'span' | |||||
const Border = styled('span')({ | |||||
borderColor: 'var(--color-accent, blue)', | |||||
boxSizing: 'border-box', | |||||
display: 'inline-block', | |||||
borderWidth: '0.125rem', | |||||
borderStyle: 'solid', | |||||
position: 'absolute', | |||||
top: 0, | |||||
left: 0, | |||||
width: '100%', | |||||
height: '100%', | |||||
borderRadius: 'inherit', | |||||
zIndex: 2, | |||||
pointerEvents: 'none', | |||||
transitionProperty: 'border-color', | |||||
'::before': { | |||||
position: 'absolute', | |||||
top: 0, | |||||
left: 0, | |||||
width: '100%', | |||||
height: '100%', | |||||
content: "''", | |||||
borderRadius: '0.125rem', | |||||
opacity: 0.5, | |||||
pointerEvents: 'none', | |||||
}, | |||||
[`${ComponentBase}:focus-within &::before`]: { | |||||
boxShadow: '0 0 0 0.375rem var(--color-accent, blue)', | |||||
}, | |||||
}) | |||||
const Input = styled('input')({ | |||||
display: 'block', | |||||
paddingTop: 0, | |||||
paddingBottom: 0, | |||||
boxSizing: 'border-box', | |||||
position: 'relative', | |||||
border: 0, | |||||
borderRadius: 'inherit', | |||||
margin: 0, | |||||
font: 'inherit', | |||||
minHeight: '4rem', | |||||
minWidth: '8rem', | |||||
maxWidth: '100%', | |||||
width: '100%', | |||||
zIndex: 1, | |||||
transitionProperty: 'background-color, color', | |||||
':focus': { | |||||
outline: 0, | |||||
color: 'var(--color-fg, black)', | |||||
}, | |||||
':disabled': { | |||||
cursor: 'not-allowed', | |||||
}, | |||||
'@media only screen': { | |||||
backgroundColor: 'var(--color-bg, white)', | |||||
color: 'var(--color-fg, black)', | |||||
}, | |||||
}) | |||||
Input.displayName = 'input' | |||||
const TextArea = styled('textarea')({ | |||||
display: 'block', | |||||
verticalAlign: 'top', | |||||
width: '100%', | |||||
boxSizing: 'border-box', | |||||
position: 'relative', | |||||
border: 0, | |||||
borderRadius: 'inherit', | |||||
margin: 0, | |||||
font: 'inherit', | |||||
minHeight: '4rem', | |||||
minWidth: '3rem', | |||||
maxWidth: '100%', | |||||
zIndex: 1, | |||||
transitionProperty: 'background-color, color', | |||||
':focus': { | |||||
outline: 0, | |||||
}, | |||||
'@media only screen': { | |||||
backgroundColor: 'var(--color-bg, white)', | |||||
color: 'var(--color-fg, black)', | |||||
}, | |||||
}) | |||||
TextArea.displayName = 'textarea' | |||||
const HintWrapper = styled('span')({ | |||||
boxSizing: 'border-box', | |||||
position: 'absolute', | |||||
left: 0, | |||||
fontSize: '0.85em', | |||||
opacity: 0.5, | |||||
maxWidth: '100%', | |||||
overflow: 'hidden', | |||||
textOverflow: 'ellipsis', | |||||
whiteSpace: 'nowrap', | |||||
zIndex: 2, | |||||
pointerEvents: 'none', | |||||
lineHeight: 1, | |||||
userSelect: 'none', | |||||
}) | |||||
const IndicatorWrapper = styled('span')({ | |||||
color: 'var(--color-accent, blue)', | |||||
boxSizing: 'border-box', | |||||
position: 'absolute', | |||||
bottom: 0, | |||||
right: 0, | |||||
display: 'grid', | |||||
placeContent: 'center', | |||||
padding: '0 1rem', | |||||
zIndex: 1, | |||||
pointerEvents: 'none', | |||||
transitionProperty: 'color', | |||||
lineHeight: 1, | |||||
userSelect: 'none', | |||||
}) | |||||
export type Props = Omit<React.HTMLProps<HTMLInputElement>, 'as' | 'className' | 'style' | 'placeholder' | 'size'> & { | |||||
/** | |||||
* Short textual description indicating the nature of the component's value. | |||||
*/ | |||||
label?: React.ReactNode, | |||||
/** | |||||
* Short textual description as guidelines for valid input values. | |||||
*/ | |||||
hint?: React.ReactNode, | |||||
/** | |||||
* Size of the component. | |||||
*/ | |||||
size?: TextInputSize, | |||||
/** | |||||
* Additional description, usually graphical, indicating the nature of the component's value. | |||||
*/ | |||||
indicator?: React.ReactNode, | |||||
/** | |||||
* Should the component display a border? | |||||
*/ | |||||
border?: boolean, | |||||
/** | |||||
* Should the component be displayed with an alternate appearance? | |||||
*/ | |||||
alternate?: boolean, | |||||
/** | |||||
* Should the component occupy the whole width of its parent? | |||||
*/ | |||||
block?: boolean, | |||||
} | |||||
/** | |||||
* Component for inputting textual values. | |||||
* | |||||
* This component supports multiline input and adjusts its layout accordingly. | |||||
*/ | |||||
export const TextInput = React.forwardRef<HTMLInputElement, Props>( | |||||
( | |||||
{ | |||||
label = '', | |||||
hint = '', | |||||
indicator = null, | |||||
size = TextInputSize.MEDIUM, | |||||
disabled = false, | |||||
border = false, | |||||
alternate = false, | |||||
block = false, | |||||
...etcProps | |||||
}, | |||||
ref, | |||||
) => ( | |||||
<ComponentBase | |||||
style={{ | |||||
display: block ? 'block' : 'inline-block', | |||||
opacity: disabled ? 0.5 : undefined, | |||||
}} | |||||
> | |||||
{border && <Border />} | |||||
<CaptureArea> | |||||
{label && ( | |||||
<> | |||||
<LabelWrapper | |||||
style={{ | |||||
paddingLeft: alternate ? (border ? '0.5rem' : undefined) : '0.5rem', | |||||
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingRight: indicator ? MIN_HEIGHTS[size!] : (alternate ? (border ? '0.5rem' : undefined) : '0.5rem'), | |||||
fontSize: SECONDARY_TEXT_SIZES[size!], | |||||
}} | |||||
> | |||||
{label} | |||||
</LabelWrapper> | |||||
{' '} | |||||
</> | |||||
)} | |||||
<Input | |||||
{...etcProps} | |||||
ref={ref as React.Ref<HTMLInputElement>} | |||||
disabled={disabled!} | |||||
style={{ | |||||
paddingLeft: alternate ? (border ? '0.5rem' : undefined) : '1rem', | |||||
fontSize: INPUT_FONT_SIZES[size!], | |||||
minHeight: MIN_HEIGHTS[size!], | |||||
paddingTop: alternate ? `calc(${SECONDARY_TEXT_SIZES[size!]} * 2)`: undefined, | |||||
paddingRight: indicator ? MIN_HEIGHTS[size!] : (alternate ? (border ? '0.5rem' : undefined) : '1rem'), | |||||
}} | |||||
aria-label={label} | |||||
/> | |||||
</CaptureArea> | |||||
{hint && ( | |||||
<> | |||||
{' '} | |||||
<HintWrapper | |||||
style={{ | |||||
top: alternate ? '0.75rem' : undefined, | |||||
bottom: alternate ? undefined : 0, | |||||
paddingLeft: alternate ? (border ? '0.5rem' : undefined) : '1rem', | |||||
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!], | |||||
paddingRight: indicator ? MIN_HEIGHTS[size!] : (alternate ? (border ? '0.5rem' : undefined) : '1rem'), | |||||
fontSize: SECONDARY_TEXT_SIZES[size!], | |||||
}} | |||||
> | |||||
{hint} | |||||
</HintWrapper> | |||||
</> | |||||
)} | |||||
{indicator && ( | |||||
<IndicatorWrapper | |||||
style={{ | |||||
width: MIN_HEIGHTS[size!], | |||||
height: MIN_HEIGHTS[size!], | |||||
}} | |||||
> | |||||
{indicator} | |||||
</IndicatorWrapper> | |||||
)} | |||||
</ComponentBase> | |||||
), | |||||
) | |||||
TextInput.displayName = 'TextInput' |
@@ -1,17 +1,8 @@ | |||||
import Button from './components/Button/Button' | |||||
import Checkbox from './components/Checkbox/Checkbox' | |||||
import Icon from './components/Icon/Icon' | |||||
import RadioButton from './components/RadioButton/RadioButton' | |||||
import Select from './components/Select/Select' | |||||
import Slider from './components/Slider/Slider' | |||||
import TextInput from './components/TextInput/TextInput' | |||||
export { | |||||
Button, | |||||
Checkbox, | |||||
Icon, | |||||
RadioButton, | |||||
Select, | |||||
Slider, | |||||
TextInput, | |||||
} | |||||
export * from './components/Button' | |||||
export * from './components/Checkbox' | |||||
export * from './components/Icon' | |||||
export * from './components/MultilineTextInput' | |||||
export * from './components/RadioButton' | |||||
export * from './components/Select' | |||||
export * from './components/Slider' | |||||
export * from './components/TextInput' |
@@ -0,0 +1,37 @@ | |||||
import React from 'react'; | |||||
import { Story, Meta } from '@storybook/react'; | |||||
import { Button, ButtonProps } from './Button'; | |||||
export default { | |||||
title: 'Example/Button', | |||||
component: Button, | |||||
argTypes: { | |||||
backgroundColor: { control: 'color' }, | |||||
}, | |||||
} as Meta; | |||||
const Template: Story<ButtonProps> = (args) => <Button {...args} />; | |||||
export const Primary = Template.bind({}); | |||||
Primary.args = { | |||||
primary: true, | |||||
label: 'Button', | |||||
}; | |||||
export const Secondary = Template.bind({}); | |||||
Secondary.args = { | |||||
label: 'Button', | |||||
}; | |||||
export const Large = Template.bind({}); | |||||
Large.args = { | |||||
size: 'large', | |||||
label: 'Button', | |||||
}; | |||||
export const Small = Template.bind({}); | |||||
Small.args = { | |||||
size: 'small', | |||||
label: 'Button', | |||||
}; |
@@ -0,0 +1,48 @@ | |||||
import React from 'react'; | |||||
import './button.css'; | |||||
export interface ButtonProps { | |||||
/** | |||||
* Is this the principal call to action on the page? | |||||
*/ | |||||
primary?: boolean; | |||||
/** | |||||
* What background color to use | |||||
*/ | |||||
backgroundColor?: string; | |||||
/** | |||||
* How large should the button be? | |||||
*/ | |||||
size?: 'small' | 'medium' | 'large'; | |||||
/** | |||||
* Button contents | |||||
*/ | |||||
label: string; | |||||
/** | |||||
* Optional click handler | |||||
*/ | |||||
onClick?: () => void; | |||||
} | |||||
/** | |||||
* Primary UI component for user interaction | |||||
*/ | |||||
export const Button: React.FC<ButtonProps> = ({ | |||||
primary = false, | |||||
size = 'medium', | |||||
backgroundColor, | |||||
label, | |||||
...props | |||||
}) => { | |||||
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; | |||||
return ( | |||||
<button | |||||
type="button" | |||||
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')} | |||||
style={{ backgroundColor }} | |||||
{...props} | |||||
> | |||||
{label} | |||||
</button> | |||||
); | |||||
}; |
@@ -0,0 +1,19 @@ | |||||
import React from 'react'; | |||||
import { Story, Meta } from '@storybook/react'; | |||||
import { Header, HeaderProps } from './Header'; | |||||
export default { | |||||
title: 'Example/Header', | |||||
component: Header, | |||||
} as Meta; | |||||
const Template: Story<HeaderProps> = (args) => <Header {...args} />; | |||||
export const LoggedIn = Template.bind({}); | |||||
LoggedIn.args = { | |||||
user: {}, | |||||
}; | |||||
export const LoggedOut = Template.bind({}); | |||||
LoggedOut.args = {}; |
@@ -0,0 +1,47 @@ | |||||
import React from 'react'; | |||||
import { Button } from './Button'; | |||||
import './header.css'; | |||||
export interface HeaderProps { | |||||
user?: {}; | |||||
onLogin: () => void; | |||||
onLogout: () => void; | |||||
onCreateAccount: () => void; | |||||
} | |||||
export const Header: React.FC<HeaderProps> = ({ user, onLogin, onLogout, onCreateAccount }) => ( | |||||
<header> | |||||
<div className="wrapper"> | |||||
<div> | |||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> | |||||
<g fill="none" fillRule="evenodd"> | |||||
<path | |||||
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z" | |||||
fill="#FFF" | |||||
/> | |||||
<path | |||||
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z" | |||||
fill="#555AB9" | |||||
/> | |||||
<path | |||||
d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" | |||||
fill="#91BAF8" | |||||
/> | |||||
</g> | |||||
</svg> | |||||
<h1>Acme</h1> | |||||
</div> | |||||
<div> | |||||
{user ? ( | |||||
<Button size="small" onClick={onLogout} label="Log out" /> | |||||
) : ( | |||||
<> | |||||
<Button size="small" onClick={onLogin} label="Log in" /> | |||||
<Button primary size="small" onClick={onCreateAccount} label="Sign up" /> | |||||
</> | |||||
)} | |||||
</div> | |||||
</div> | |||||
</header> | |||||
); |
@@ -0,0 +1,211 @@ | |||||
import { Meta } from '@storybook/addon-docs/blocks'; | |||||
import Code from './assets/code-brackets.svg'; | |||||
import Colors from './assets/colors.svg'; | |||||
import Comments from './assets/comments.svg'; | |||||
import Direction from './assets/direction.svg'; | |||||
import Flow from './assets/flow.svg'; | |||||
import Plugin from './assets/plugin.svg'; | |||||
import Repo from './assets/repo.svg'; | |||||
import StackAlt from './assets/stackalt.svg'; | |||||
<Meta title="Example/Introduction" /> | |||||
<style>{` | |||||
.subheading { | |||||
--mediumdark: '#999999'; | |||||
font-weight: 900; | |||||
font-size: 13px; | |||||
color: #999; | |||||
letter-spacing: 6px; | |||||
line-height: 24px; | |||||
text-transform: uppercase; | |||||
margin-bottom: 12px; | |||||
margin-top: 40px; | |||||
} | |||||
.link-list { | |||||
display: grid; | |||||
grid-template-columns: 1fr; | |||||
grid-template-rows: 1fr 1fr; | |||||
row-gap: 10px; | |||||
} | |||||
@media (min-width: 620px) { | |||||
.link-list { | |||||
row-gap: 20px; | |||||
column-gap: 20px; | |||||
grid-template-columns: 1fr 1fr; | |||||
} | |||||
} | |||||
@media all and (-ms-high-contrast:none) { | |||||
.link-list { | |||||
display: -ms-grid; | |||||
-ms-grid-columns: 1fr 1fr; | |||||
-ms-grid-rows: 1fr 1fr; | |||||
} | |||||
} | |||||
.link-item { | |||||
display: block; | |||||
padding: 20px 30px 20px 15px; | |||||
border: 1px solid #00000010; | |||||
border-radius: 5px; | |||||
transition: background 150ms ease-out, border 150ms ease-out, transform 150ms ease-out; | |||||
color: #333333; | |||||
display: flex; | |||||
align-items: flex-start; | |||||
} | |||||
.link-item:hover { | |||||
border-color: #1EA7FD50; | |||||
transform: translate3d(0, -3px, 0); | |||||
box-shadow: rgba(0, 0, 0, 0.08) 0 3px 10px 0; | |||||
} | |||||
.link-item:active { | |||||
border-color: #1EA7FD; | |||||
transform: translate3d(0, 0, 0); | |||||
} | |||||
.link-item strong { | |||||
font-weight: 700; | |||||
display: block; | |||||
margin-bottom: 2px; | |||||
} | |||||
.link-item img { | |||||
height: 40px; | |||||
width: 40px; | |||||
margin-right: 15px; | |||||
flex: none; | |||||
} | |||||
.link-item span { | |||||
font-size: 14px; | |||||
line-height: 20px; | |||||
} | |||||
.tip { | |||||
display: inline-block; | |||||
border-radius: 1em; | |||||
font-size: 11px; | |||||
line-height: 12px; | |||||
font-weight: 700; | |||||
background: #E7FDD8; | |||||
color: #66BF3C; | |||||
padding: 4px 12px; | |||||
margin-right: 10px; | |||||
vertical-align: top; | |||||
} | |||||
.tip-wrapper { | |||||
font-size: 13px; | |||||
line-height: 20px; | |||||
margin-top: 40px; | |||||
margin-bottom: 40px; | |||||
} | |||||
.tip-wrapper code { | |||||
font-size: 12px; | |||||
display: inline-block; | |||||
} | |||||
`}</style> | |||||
# Welcome to Storybook | |||||
Storybook helps you build UI components in isolation from your app's business logic, data, and context. | |||||
That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA. | |||||
Browse example stories now by navigating to them in the sidebar. | |||||
View their code in the `src/stories` directory to learn how they work. | |||||
We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages. | |||||
<div className="subheading">Configure</div> | |||||
<div className="link-list"> | |||||
<a | |||||
className="link-item" | |||||
href="https://storybook.js.org/docs/react/addons/addon-types" | |||||
target="_blank" | |||||
> | |||||
<img src={Plugin} alt="plugin" /> | |||||
<span> | |||||
<strong>Presets for popular tools</strong> | |||||
Easy setup for TypeScript, SCSS and more. | |||||
</span> | |||||
</a> | |||||
<a | |||||
className="link-item" | |||||
href="https://storybook.js.org/docs/react/configure/webpack" | |||||
target="_blank" | |||||
> | |||||
<img src={StackAlt} alt="Build" /> | |||||
<span> | |||||
<strong>Build configuration</strong> | |||||
How to customize webpack and Babel | |||||
</span> | |||||
</a> | |||||
<a | |||||
className="link-item" | |||||
href="https://storybook.js.org/docs/react/configure/styling-and-css" | |||||
target="_blank" | |||||
> | |||||
<img src={Colors} alt="colors" /> | |||||
<span> | |||||
<strong>Styling</strong> | |||||
How to load and configure CSS libraries | |||||
</span> | |||||
</a> | |||||
<a | |||||
className="link-item" | |||||
href="https://storybook.js.org/docs/react/get-started/setup#configure-storybook-for-your-stack" | |||||
target="_blank" | |||||
> | |||||
<img src={Flow} alt="flow" /> | |||||
<span> | |||||
<strong>Data</strong> | |||||
Providers and mocking for data libraries | |||||
</span> | |||||
</a> | |||||
</div> | |||||
<div className="subheading">Learn</div> | |||||
<div className="link-list"> | |||||
<a className="link-item" href="https://storybook.js.org/docs" target="_blank"> | |||||
<img src={Repo} alt="repo" /> | |||||
<span> | |||||
<strong>Storybook documentation</strong> | |||||
Configure, customize, and extend | |||||
</span> | |||||
</a> | |||||
<a className="link-item" href="https://storybook.js.org/tutorials/" target="_blank"> | |||||
<img src={Direction} alt="direction" /> | |||||
<span> | |||||
<strong>In-depth guides</strong> | |||||
Best practices from leading teams | |||||
</span> | |||||
</a> | |||||
<a className="link-item" href="https://github.com/storybookjs/storybook" target="_blank"> | |||||
<img src={Code} alt="code" /> | |||||
<span> | |||||
<strong>GitHub project</strong> | |||||
View the source and add issues | |||||
</span> | |||||
</a> | |||||
<a className="link-item" href="https://discord.gg/storybook" target="_blank"> | |||||
<img src={Comments} alt="comments" /> | |||||
<span> | |||||
<strong>Discord chat</strong> | |||||
Chat with maintainers and the community | |||||
</span> | |||||
</a> | |||||
</div> | |||||
<div className="tip-wrapper"> | |||||
<span className="tip">Tip</span>Edit the Markdown in{' '} | |||||
<code>src/stories/Introduction.stories.mdx</code> | |||||
</div> |
@@ -0,0 +1,22 @@ | |||||
import React from 'react'; | |||||
import { Story, Meta } from '@storybook/react'; | |||||
import { Page, PageProps } from './Page'; | |||||
import * as HeaderStories from './Header.stories'; | |||||
export default { | |||||
title: 'Example/Page', | |||||
component: Page, | |||||
} as Meta; | |||||
const Template: Story<PageProps> = (args) => <Page {...args} />; | |||||
export const LoggedIn = Template.bind({}); | |||||
LoggedIn.args = { | |||||
...HeaderStories.LoggedIn.args, | |||||
}; | |||||
export const LoggedOut = Template.bind({}); | |||||
LoggedOut.args = { | |||||
...HeaderStories.LoggedOut.args, | |||||
}; |
@@ -0,0 +1,67 @@ | |||||
import React from 'react'; | |||||
import { Header } from './Header'; | |||||
import './page.css'; | |||||
export interface PageProps { | |||||
user?: {}; | |||||
onLogin: () => void; | |||||
onLogout: () => void; | |||||
onCreateAccount: () => void; | |||||
} | |||||
export const Page: React.FC<PageProps> = ({ user, onLogin, onLogout, onCreateAccount }) => ( | |||||
<article> | |||||
<Header user={user} onLogin={onLogin} onLogout={onLogout} onCreateAccount={onCreateAccount} /> | |||||
<section> | |||||
<h2>Pages in Storybook</h2> | |||||
<p> | |||||
We recommend building UIs with a{' '} | |||||
<a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer"> | |||||
<strong>component-driven</strong> | |||||
</a>{' '} | |||||
process starting with atomic components and ending with pages. | |||||
</p> | |||||
<p> | |||||
Render pages with mock data. This makes it easy to build and review page states without | |||||
needing to navigate to them in your app. Here are some handy patterns for managing page data | |||||
in Storybook: | |||||
</p> | |||||
<ul> | |||||
<li> | |||||
Use a higher-level connected component. Storybook helps you compose such data from the | |||||
"args" of child component stories | |||||
</li> | |||||
<li> | |||||
Assemble data in the page component from your services. You can mock these services out | |||||
using Storybook. | |||||
</li> | |||||
</ul> | |||||
<p> | |||||
Get a guided tutorial on component-driven development at{' '} | |||||
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer"> | |||||
Storybook tutorials | |||||
</a> | |||||
. Read more in the{' '} | |||||
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer"> | |||||
docs | |||||
</a> | |||||
. | |||||
</p> | |||||
<div className="tip-wrapper"> | |||||
<span className="tip">Tip</span> Adjust the width of the canvas with the{' '} | |||||
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"> | |||||
<g fill="none" fillRule="evenodd"> | |||||
<path | |||||
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z" | |||||
id="a" | |||||
fill="#999" | |||||
/> | |||||
</g> | |||||
</svg> | |||||
Viewports addon in the toolbar | |||||
</div> | |||||
</section> | |||||
</article> | |||||
); |
@@ -0,0 +1 @@ | |||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/code-brackets</title><g id="illustration/code-brackets" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#87E6E5" d="M11.4139325,12 C11.7605938,12 12,12.5059743 12,13.3779712 L12,17.4951758 L6.43502246,23.3839989 C5.85499251,23.9978337 5.85499251,25.0021663 6.43502246,25.6160011 L12,31.5048242 L12,35.6220288 C12,36.4939606 11.7605228,37 11.4139325,37 C11.2725831,37 11.1134406,36.9158987 10.9453839,36.7379973 L0.435022463,25.6160011 C-0.145007488,25.0021663 -0.145007488,23.9978337 0.435022463,23.3839989 L10.9453839,12.2620027 C11.1134051,12.0841663 11.2725831,12 11.4139325,12 Z M36.5860675,12 C36.7274169,12 36.8865594,12.0841013 37.0546161,12.2620027 L47.5649775,23.3839989 C48.1450075,23.9978337 48.1450075,25.0021663 47.5649775,25.6160011 L37.0546161,36.7379973 C36.8865949,36.9158337 36.7274169,37 36.5860675,37 C36.2394062,37 36,36.4940257 36,35.6220288 L36,31.5048242 L41.5649775,25.6160011 C42.1450075,25.0021663 42.1450075,23.9978337 41.5649775,23.3839989 L36,17.4951758 L36,13.3779712 C36,12.5060394 36.2394772,12 36.5860675,12 Z"/><rect id="Rectangle-7-Copy-5" width="35.57" height="4" x="5.009" y="22.662" fill="#A0DB77" rx="2" transform="translate(22.793959, 24.662305) rotate(-75.000000) translate(-22.793959, -24.662305)"/></g></svg> |
@@ -0,0 +1 @@ | |||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/comments</title><g id="illustration/comments" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Path" fill="#96D07C" d="M2.52730803,17.9196415 C2.44329744,17.9745167 2.36370847,18.000488 2.29303375,18.000488 C2.1197031,18.000488 2,17.8443588 2,17.5752855 L2,4 C2,1.790861 3.790861,3.23296945e-13 6,3.23296945e-13 L33.9995117,3.23296945e-13 C36.2086507,3.23296945e-13 37.9995117,1.790861 37.9995117,4 L37.9995117,9.999512 C37.9995117,12.208651 36.2086507,13.999512 33.9995117,13.999512 L8,13.999512 C7.83499225,13.999512 7.6723181,13.9895206 7.51254954,13.9701099 L2.52730803,17.9196415 Z"/><path id="Path" fill="#73E1E0" d="M7.51066,44.9703679 L2.52730803,47.9186655 C2.44329744,47.9735407 2.36370847,47.999512 2.29303375,47.999512 C2.1197031,47.999512 2,47.8433828 2,47.5743095 L2,35 C2,32.790861 3.790861,31 6,31 L26,31 C28.209139,31 30,32.790861 30,35 L30,41 C30,43.209139 28.209139,45 26,45 L8,45 C7.8343417,45 7.67103544,44.9899297 7.51066,44.9703679 Z"/><path id="Path" fill="#FFD476" d="M46,19.5 L46,33.0747975 C46,33.3438708 45.8802969,33.5 45.7069663,33.5 C45.6362915,33.5 45.5567026,33.4740287 45.472692,33.4191535 L40.4887103,29.4704446 C40.3285371,29.489956 40.1654415,29.5 40,29.5 L18,29.5 C15.790861,29.5 14,27.709139 14,25.5 L14,19.5 C14,17.290861 15.790861,15.5 18,15.5 L42,15.5 C44.209139,15.5 46,17.290861 46,19.5 Z"/></g></svg> |
@@ -0,0 +1 @@ | |||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/direction</title><g id="illustration/direction" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFD476" d="M23.4917015,33.6030641 L2.93840258,31.4321033 C2.38917316,31.3740904 1.99096346,30.8818233 2.04897631,30.3325939 C2.0747515,30.0885705 2.18934861,29.8625419 2.37095722,29.6975265 L34.2609105,0.721285325 C34.6696614,0.349881049 35.3021022,0.38015648 35.6735064,0.788907393 C35.9232621,1.06377731 36.0001133,1.45442096 35.8730901,1.80341447 L24.5364357,32.9506164 C24.3793473,33.3822133 23.9484565,33.6513092 23.4917015,33.6030641 L23.4917015,33.6030641 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" d="M24.3163597,33.2881029 C24.0306575,33.0138462 23.9337246,32.5968232 24.069176,32.2246735 L35.091923,1.9399251 C35.2266075,1.56988243 35.5659249,1.31333613 35.9586669,1.28460955 C36.5094802,1.24432106 36.9886628,1.65818318 37.0289513,2.20899647 L40.2437557,46.1609256 C40.2644355,46.4436546 40.1641446,46.7218752 39.9678293,46.9263833 C39.5853672,47.3248067 38.9523344,47.3377458 38.5539111,46.9552837 L24.3163597,33.2881029 L24.3163597,33.2881029 Z"/></g></svg> |
@@ -0,0 +1 @@ | |||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/flow</title><g id="illustration/flow" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" fill-rule="nonzero" d="M30,29 C32.7614237,29 35,26.7614237 35,24 C35,14.6111593 27.3888407,7 18,7 C8.61115925,7 1,14.6111593 1,24 C1,33.3888407 8.61115925,41 18,41 C19.3333404,41 20.6447683,40.8466238 21.9154603,40.5471706 C19.5096374,39.3319645 17.5510566,37.8612875 16.0456579,36.1314815 C14.1063138,33.9030427 12.769443,31.0725999 12.0293806,27.6556449 C11.360469,26.565281 11,25.3082308 11,24 C11,20.1340068 14.1340068,17 18,17 C21.8659932,17 25,20.1340068 25,24 C25,26.125 27.7040312,29 30,29 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" fill-rule="nonzero" d="M42,29 C44.7614237,29 47,26.7614237 47,24 C47,14.6111593 39.3888407,7 30,7 C20.6111593,7 13,14.6111593 13,24 C13,33.3888407 20.6111593,41 30,41 C31.3333404,41 32.6447683,40.8466238 33.9154603,40.5471706 C31.5096374,39.3319645 29.4051056,37.9781963 28.0456579,36.1314815 C26.0625,33.4375 23,27.1875 23,24 C23,20.1340068 26.1340068,17 30,17 C33.8659932,17 37,20.1340068 37,24 C37.02301,26.3435241 39.7040312,29 42,29 Z" transform="translate(30.000000, 24.000000) scale(-1, -1) translate(-30.000000, -24.000000)"/></g></svg> |
@@ -0,0 +1 @@ | |||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/plugin</title><g id="illustration/plugin" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" d="M26,15.3994248 C26,15.4091303 26,15.4188459 26,15.4285714 L26,21.4694881 C25.8463595,21.4969567 25.6941676,21.51275 25.5873784,21.51275 C25.4974117,21.51275 25.4230979,21.4768034 25.377756,21.4206259 L25.2660784,21.2822603 L25.1317423,21.1657666 C24.2436317,20.3956144 23.100098,19.9633214 21.895551,19.9633214 C19.2039137,19.9633214 17,22.1075558 17,24.7804643 C17,27.4533728 19.2039137,29.5976071 21.895551,29.5976071 C23.1972122,29.5976071 24.3149423,29.2878193 25.1231445,28.3613697 C25.4542273,27.9818463 25.568273,27.9073214 25.5873784,27.9073214 C25.681532,27.9073214 25.8352452,27.9239643 26,27.9524591 L26,32.5714286 C26,32.5811541 26,32.5908697 26,32.6005752 L26,33 C26,35.209139 24.209139,37 22,37 L4,37 C1.790861,37 0,35.209139 0,33 L0,15 C0,12.790861 1.790861,11 4,11 L22,11 C24.209139,11 26,12.790861 26,15 L26,15.3994248 Z"/><path id="Path" fill="#87E6E5" d="M27.9998779,32.5714286 C27.9998779,33.3604068 28.6572726,34 29.4682101,34 L46.5315458,34 C47.3424832,34 47.9998779,33.3604068 47.9998779,32.5714286 L47.9998779,15.4285714 C47.9998779,14.6395932 47.3424832,14 46.5315458,14 L29.4682101,14 C28.6572726,14 27.9998779,14.6395932 27.9998779,15.4285714 L27.9998779,21.8355216 C27.9334367,22.2650514 27.8567585,22.6454496 27.746391,22.8084643 C27.4245309,23.2838571 26.2402709,23.51275 25.5873784,23.51275 C24.8705773,23.51275 24.2322714,23.1857725 23.8214379,22.6767605 C23.3096996,22.2329909 22.6349941,21.9633214 21.895551,21.9633214 C20.2963823,21.9633214 19,23.2245992 19,24.7804643 C19,26.3363293 20.2963823,27.5976071 21.895551,27.5976071 C22.5398535,27.5976071 23.2399343,27.477727 23.6160247,27.0466112 C24.1396029,26.4464286 24.7367044,25.9073214 25.5873784,25.9073214 C26.2402709,25.9073214 27.5912951,26.1766031 27.8226692,26.6116071 C27.8819199,26.7230038 27.9403239,26.921677 27.9998779,27.1556219 L27.9998779,32.5714286 Z"/></g></svg> |
@@ -0,0 +1 @@ | |||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/repo</title><g id="illustration/repo" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Rectangle-62-Copy" fill="#B7F0EF" d="M27.2217723,9.04506931 L41.2217723,6.2682098 C43.3886973,5.83840648 45.4937616,7.2466219 45.9235649,9.41354696 C45.9743993,9.66983721 46,9.93049166 46,10.1917747 L46,32.581381 C46,34.4904961 44.650862,36.1335143 42.7782277,36.5049459 L28.7782277,39.2818054 C26.6113027,39.7116087 24.5062384,38.3033933 24.0764351,36.1364682 C24.0256007,35.880178 24,35.6195235 24,35.3582405 L24,12.9686342 C24,11.0595191 25.349138,9.4165009 27.2217723,9.04506931 Z" opacity=".7"/><path id="Combined-Shape" fill="#87E6E5" d="M6.77822775,6.2682098 L20.7782277,9.04506931 C22.650862,9.4165009 24,11.0595191 24,12.9686342 L24,35.3582405 C24,37.5673795 22.209139,39.3582405 20,39.3582405 C19.738717,39.3582405 19.4780625,39.3326398 19.2217723,39.2818054 L5.22177225,36.5049459 C3.34913798,36.1335143 2,34.4904961 2,32.581381 L2,10.1917747 C2,7.98263571 3.790861,6.19177471 6,6.19177471 C6.26128305,6.19177471 6.5219375,6.21737537 6.77822775,6.2682098 Z"/><path id="Rectangle-63-Copy-2" fill="#61C1FD" d="M22,10 C23.1666667,10.2291667 24.0179036,10.625 24.5537109,11.1875 C25.0895182,11.75 25.5716146,12.875 26,14.5625 C26,29.3020833 26,37.5208333 26,39.21875 C26,40.9166667 26.4241536,42.9583333 27.2724609,45.34375 L24.5537109,41.875 L22.9824219,45.34375 C22.327474,43.1979167 22,41.2291667 22,39.4375 C22,37.6458333 22,27.8333333 22,10 Z"/></g></svg> |
@@ -0,0 +1 @@ | |||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/stackalt</title><g id="illustration/stackalt" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFAE00" d="M23.8628277,0 L23.8628277,48 L3.32291648,36.2491883 L3.32155653,11.9499781 L23.8628277,0 Z M23.8670509,0 L44.408322,11.9499781 L44.4069621,36.2491883 L23.8670509,48 L23.8670509,0 Z" opacity=".196"/><path id="Rectangle-46-Copy-3" fill="#66BF3C" d="M15.8232279,19.1155258 L24.7368455,21.4714881 C29.6053842,22.7582937 33.4077423,26.5606518 34.694548,31.4291905 L37.0505103,40.3428082 C37.6150232,42.4786032 36.3412474,44.6676353 34.2054524,45.2321482 C33.5569474,45.4035549 32.87575,45.4091235 32.2245294,45.2483418 L23.3459013,43.0562718 C18.2976962,41.809906 14.3561301,37.8683399 13.1097642,32.8201348 L10.9176943,23.9415066 C10.3881737,21.7967682 11.6975664,19.6288529 13.8423049,19.0993322 C14.4935255,18.9385505 15.1747229,18.9441191 15.8232279,19.1155258 Z" opacity=".5" transform="translate(23.999997, 32.166058) rotate(-45.000000) translate(-23.999997, -32.166058)"/><path id="Rectangle-46-Copy-2" fill="#FFAE00" d="M15.8232279,11.2216893 L24.7368455,13.5776516 C29.6053842,14.8644572 33.4077423,18.6668153 34.694548,23.5353541 L37.0505103,32.4489717 C37.6150232,34.5847667 36.3412474,36.7737988 34.2054524,37.3383117 C33.5569474,37.5097184 32.87575,37.515287 32.2245294,37.3545053 L23.3459013,35.1624353 C18.2976962,33.9160695 14.3561301,29.9745034 13.1097642,24.9262983 L10.9176943,16.0476701 C10.3881737,13.9029317 11.6975664,11.7350164 13.8423049,11.2054957 C14.4935255,11.044714 15.1747229,11.0502826 15.8232279,11.2216893 Z" opacity=".5" transform="translate(23.999997, 24.272222) rotate(-45.000000) translate(-23.999997, -24.272222)"/><path id="Rectangle-46-Copy" fill="#FC521F" d="M15.8232279,3.32785281 L24.7368455,5.68381509 C29.6053842,6.97062075 33.4077423,10.7729788 34.694548,15.6415176 L37.0505103,24.5551352 C37.6150232,26.6909302 36.3412474,28.8799623 34.2054524,29.4444752 C33.5569474,29.6158819 32.87575,29.6214505 32.2245294,29.4606688 L23.3459013,27.2685988 C18.2976962,26.022233 14.3561301,22.0806669 13.1097642,17.0324618 L10.9176943,8.15383364 C10.3881737,6.00909519 11.6975664,3.84117987 13.8423049,3.31165925 C14.4935255,3.15087753 15.1747229,3.15644615 15.8232279,3.32785281 Z" opacity=".5" transform="translate(23.999997, 16.378385) rotate(-45.000000) translate(-23.999997, -16.378385)"/></g></svg> |
@@ -0,0 +1,30 @@ | |||||
.storybook-button { | |||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; | |||||
font-weight: 700; | |||||
border: 0; | |||||
border-radius: 3em; | |||||
cursor: pointer; | |||||
display: inline-block; | |||||
line-height: 1; | |||||
} | |||||
.storybook-button--primary { | |||||
color: white; | |||||
background-color: #1ea7fd; | |||||
} | |||||
.storybook-button--secondary { | |||||
color: #333; | |||||
background-color: transparent; | |||||
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; | |||||
} | |||||
.storybook-button--small { | |||||
font-size: 12px; | |||||
padding: 10px 16px; | |||||
} | |||||
.storybook-button--medium { | |||||
font-size: 14px; | |||||
padding: 11px 20px; | |||||
} | |||||
.storybook-button--large { | |||||
font-size: 16px; | |||||
padding: 12px 24px; | |||||
} |
@@ -0,0 +1,26 @@ | |||||
.wrapper { | |||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; | |||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1); | |||||
padding: 15px 20px; | |||||
display: flex; | |||||
align-items: center; | |||||
justify-content: space-between; | |||||
} | |||||
svg { | |||||
display: inline-block; | |||||
vertical-align: top; | |||||
} | |||||
h1 { | |||||
font-weight: 900; | |||||
font-size: 20px; | |||||
line-height: 1; | |||||
margin: 6px 0 6px 10px; | |||||
display: inline-block; | |||||
vertical-align: top; | |||||
} | |||||
button + button { | |||||
margin-left: 10px; | |||||
} |
@@ -0,0 +1,69 @@ | |||||
section { | |||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; | |||||
font-size: 14px; | |||||
line-height: 24px; | |||||
padding: 48px 20px; | |||||
margin: 0 auto; | |||||
max-width: 600px; | |||||
color: #333; | |||||
} | |||||
h2 { | |||||
font-weight: 900; | |||||
font-size: 32px; | |||||
line-height: 1; | |||||
margin: 0 0 4px; | |||||
display: inline-block; | |||||
vertical-align: top; | |||||
} | |||||
p { | |||||
margin: 1em 0; | |||||
} | |||||
a { | |||||
text-decoration: none; | |||||
color: #1ea7fd; | |||||
} | |||||
ul { | |||||
padding-left: 30px; | |||||
margin: 1em 0; | |||||
} | |||||
li { | |||||
margin-bottom: 8px; | |||||
} | |||||
.tip { | |||||
display: inline-block; | |||||
border-radius: 1em; | |||||
font-size: 11px; | |||||
line-height: 12px; | |||||
font-weight: 700; | |||||
background: #e7fdd8; | |||||
color: #66bf3c; | |||||
padding: 4px 12px; | |||||
margin-right: 10px; | |||||
vertical-align: top; | |||||
} | |||||
.tip-wrapper { | |||||
font-size: 13px; | |||||
line-height: 20px; | |||||
margin-top: 40px; | |||||
margin-bottom: 40px; | |||||
} | |||||
.tip-wrapper svg { | |||||
display: inline-block; | |||||
height: 12px; | |||||
width: 12px; | |||||
margin-right: 4px; | |||||
vertical-align: top; | |||||
margin-top: 3px; | |||||
} | |||||
.tip-wrapper svg path { | |||||
fill: #1ea7fd; | |||||
} |
@@ -0,0 +1,13 @@ | |||||
import React from 'react'; | |||||
import { render, screen } from '@testing-library/react'; | |||||
import '@testing-library/jest-dom'; | |||||
import Hello from '../src'; | |||||
describe('Example', () => { | |||||
it('should have the expected content', () => { | |||||
const greeting = 'World'; | |||||
render(<Hello greeting={greeting} />); | |||||
expect(screen.getByText(`Hello ${greeting}`)).toBeInTheDocument(); | |||||
}); | |||||
}); |
@@ -0,0 +1,21 @@ | |||||
{ | |||||
"exclude": ["node_modules"], | |||||
"include": ["src", "types", "test"], | |||||
"compilerOptions": { | |||||
"module": "ESNext", | |||||
"lib": ["DOM", "ESNext"], | |||||
"importHelpers": true, | |||||
"declaration": true, | |||||
"sourceMap": true, | |||||
"rootDir": "./", | |||||
"strict": true, | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"moduleResolution": "node", | |||||
"jsx": "react", | |||||
"esModuleInterop": true, | |||||
"target": "ES2017" | |||||
} | |||||
} |
@@ -0,0 +1,21 @@ | |||||
{ | |||||
"exclude": ["node_modules"], | |||||
"include": ["src", "types"], | |||||
"compilerOptions": { | |||||
"module": "ESNext", | |||||
"lib": ["DOM", "ESNext"], | |||||
"importHelpers": true, | |||||
"declaration": true, | |||||
"sourceMap": true, | |||||
"rootDir": "./src", | |||||
"strict": true, | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"moduleResolution": "node", | |||||
"jsx": "react", | |||||
"esModuleInterop": true, | |||||
"target": "ES2017" | |||||
} | |||||
} |
@@ -1,23 +0,0 @@ | |||||
import peerDepsExternal from 'rollup-plugin-peer-deps-external' | |||||
import { terser, } from 'rollup-plugin-terser' | |||||
import typescript from '@rollup/plugin-typescript' | |||||
import pkg from './package.json' | |||||
const ENTRY_POINT = './packages/react-common/src/index.ts' | |||||
export default { | |||||
input: ENTRY_POINT, | |||||
output: { | |||||
dir: pkg.directories.lib, | |||||
format: 'cjs', | |||||
sourcemap: true, | |||||
}, | |||||
plugins: [ | |||||
peerDepsExternal({ | |||||
includeDependencies: true, | |||||
}), | |||||
typescript(), | |||||
process.env.NODE_ENV === 'production' && terser(), | |||||
], | |||||
} |