├── .npmrc
├── src
├── components
│ ├── Icon
│ │ ├── index.js
│ │ └── Icon.js
│ ├── Select
│ │ ├── index.js
│ │ ├── Select.helpers.js
│ │ ├── Select.js
│ │ └── Select.stories.mdx
│ ├── IconInput
│ │ ├── index.js
│ │ ├── IconInput.js
│ │ └── IconInput.stories.mdx
│ ├── ProgressBar
│ │ ├── index.js
│ │ ├── ProgressBar.js
│ │ └── ProgressBar.stories.mdx
│ └── VisuallyHidden
│ │ ├── index.js
│ │ ├── VisuallyHidden.stories.mdx
│ │ └── VisuallyHidden.js
└── constants.js
├── .babelrc
├── .storybook
├── preview.js
├── preview-head.html
├── main.js
└── global.css
├── sandbox.config.json
├── .gitignore
├── package.json
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | legacy-peer-deps=true
2 |
--------------------------------------------------------------------------------
/src/components/Icon/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Icon';
2 |
--------------------------------------------------------------------------------
/src/components/Select/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Select';
2 |
--------------------------------------------------------------------------------
/src/components/IconInput/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './IconInput';
2 |
--------------------------------------------------------------------------------
/src/components/ProgressBar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ProgressBar';
2 |
--------------------------------------------------------------------------------
/src/components/VisuallyHidden/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './VisuallyHidden';
2 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | ["babel-plugin-styled-components", { "displayName": true }]
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import './global.css';
2 |
3 | export const parameters = {
4 | actions: { argTypesRegex: '^on[A-Z].*' },
5 | };
6 |
--------------------------------------------------------------------------------
/sandbox.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "template": "node",
3 | "container": {
4 | "port": 6006,
5 | "startScript": "start",
6 | "node": "16"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "stories": [
3 | "../src/**/*.stories.mdx",
4 | "../src/**/*.stories.@(js|jsx|ts|tsx)"
5 | ],
6 | "addons": [
7 | "@storybook/addon-links",
8 | "@storybook/addon-essentials",
9 | "@storybook/preset-create-react-app",
10 | '@storybook/addon-controls'
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Select/Select.helpers.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function getDisplayedValue(value, children) {
4 | const childArray = React.Children.toArray(children);
5 | const selectedChild = childArray.find(
6 | (child) => child.props.value === value
7 | );
8 |
9 | return selectedChild.props.children;
10 | }
11 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const COLORS = {
2 | primary: 'hsl(240deg 80% 60%)',
3 | gray50: 'hsl(0deg 0% 95%)',
4 | gray300: 'hsl(0deg 0% 75%)',
5 | gray500: 'hsl(0deg 0% 50%)',
6 | gray700: 'hsl(0deg 0% 40%)',
7 | black: 'hsl(0deg 0% 0%)',
8 | transparentGray15: 'hsl(0deg 0% 50% / 0.15)',
9 | transparentGray35: 'hsl(0deg 0% 50% / 0.35)',
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/ProgressBar/ProgressBar.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import React from 'react';
3 | import styled from 'styled-components';
4 |
5 | import { COLORS } from '../../constants';
6 | import VisuallyHidden from '../VisuallyHidden';
7 |
8 | const ProgressBar = ({ value, size }) => {
9 | return {value};
10 | };
11 |
12 | export default ProgressBar;
13 |
--------------------------------------------------------------------------------
/src/components/IconInput/IconInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | import { COLORS } from '../../constants';
5 |
6 | import Icon from '../Icon';
7 | import VisuallyHidden from '../VisuallyHidden';
8 |
9 | const IconInput = ({
10 | label,
11 | icon,
12 | width = 250,
13 | size,
14 | placeholder,
15 | }) => {
16 | return 'TODO';
17 | };
18 |
19 | export default IconInput;
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .eslintcache
25 |
--------------------------------------------------------------------------------
/src/components/VisuallyHidden/VisuallyHidden.stories.mdx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/addon-docs/blocks';
2 |
3 | import VisuallyHidden from './VisuallyHidden';
4 | import Icon from '../Icon';
5 |
6 |
7 |
8 | # VisuallyHidden
9 |
10 |
11 | <>
12 |
13 | Search
14 | >
15 |
16 |
--------------------------------------------------------------------------------
/src/components/Select/Select.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | import { COLORS } from '../../constants';
5 | import Icon from '../Icon';
6 | import { getDisplayedValue } from './Select.helpers';
7 |
8 | const Select = ({ label, value, onChange, children }) => {
9 | const displayedValue = getDisplayedValue(value, children);
10 |
11 | return (
12 |
15 | );
16 | };
17 |
18 | export default Select;
19 |
--------------------------------------------------------------------------------
/src/components/ProgressBar/ProgressBar.stories.mdx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/addon-docs/blocks';
2 | import ProgressBar from './ProgressBar';
3 |
4 |
23 |
24 | export const Template = (args) => ;
25 |
26 | # ProgressBar
27 |
28 |
29 | {Template.bind({})}
30 |
31 |
--------------------------------------------------------------------------------
/src/components/Select/Select.stories.mdx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/addon-docs/blocks';
2 | import Select from './Select';
3 |
4 |
5 |
6 | # Select
7 |
8 | export const ManagedSelect = () => {
9 | const [value, setValue] = React.useState('newest');
10 | return (
11 | <>
12 |
21 |
31 | >
32 | );
33 | };
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/IconInput/IconInput.stories.mdx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/addon-docs/blocks';
2 | import IconInput from './IconInput';
3 |
4 |
37 |
38 | export const Template = (args) => ;
39 |
40 | # IconInput
41 |
42 |
51 | {Template.bind({})}
52 |
53 |
--------------------------------------------------------------------------------
/src/components/VisuallyHidden/VisuallyHidden.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const VisuallyHidden = ({ children, ...delegated }) => {
5 | const [forceShow, setForceShow] = React.useState(false);
6 |
7 | React.useEffect(() => {
8 | if (process.env.NODE_ENV !== 'production') {
9 | const handleKeyDown = (ev) => {
10 | if (ev.key === 'Alt') {
11 | setForceShow(true);
12 | }
13 | };
14 |
15 | const handleKeyUp = () => {
16 | setForceShow(false);
17 | };
18 |
19 | window.addEventListener('keydown', handleKeyDown);
20 | window.addEventListener('keyup', handleKeyUp);
21 |
22 | return () => {
23 | window.removeEventListener('keydown', handleKeyDown);
24 | window.removeEventListener('keyup', handleKeyUp);
25 | };
26 | }
27 | }, []);
28 |
29 | if (forceShow) {
30 | return children;
31 | }
32 |
33 | return {children};
34 | };
35 |
36 | const Wrapper = styled.div`
37 | position: absolute;
38 | overflow: hidden;
39 | clip: rect(0 0 0 0);
40 | height: 1px;
41 | width: 1px;
42 | margin: -1px;
43 | padding: 0;
44 | border: 0;
45 | `;
46 |
47 | export default VisuallyHidden;
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-component-library",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "babel-loader": "8.2.2",
7 | "babel-plugin-styled-components": "1.12.0",
8 | "react": "17.0.2",
9 | "react-dom": "17.0.2",
10 | "react-feather": "2.0.9",
11 | "react-scripts": "4.0.1",
12 | "styled-components": "5.2.1"
13 | },
14 | "scripts": {
15 | "dev": "NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6006 -s public",
16 | "start": "npm run dev"
17 | },
18 | "eslintConfig": {
19 | "extends": [
20 | "react-app",
21 | "react-app/jest"
22 | ]
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | },
36 | "devDependencies": {
37 | "@storybook/addon-actions": "6.5.15",
38 | "@storybook/addon-controls": "6.5.15",
39 | "@storybook/addon-essentials": "6.5.15",
40 | "@storybook/addon-links": "6.5.15",
41 | "@storybook/node-logger": "6.5.15",
42 | "@storybook/preset-create-react-app": "3.1.7",
43 | "@storybook/react": "6.5.15",
44 | "webpack": "4.44.2"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/Icon/Icon.js:
--------------------------------------------------------------------------------
1 | /**
2 | * We're given this component "for free" since it's really more
3 | * specific to React and React Feather. Feel free to read if you're
4 | * interested, but otherwise you can rely on our docs to learn its
5 | * API / which props it takes.
6 | */
7 | import React from 'react';
8 | import styled from 'styled-components';
9 | import { Search, AtSign, ChevronDown } from 'react-feather';
10 |
11 | const icons = {
12 | search: Search,
13 | 'at-sign': AtSign,
14 | 'chevron-down': ChevronDown,
15 | };
16 |
17 | const Icon = ({ id, size, strokeWidth = 1, ...delegated }) => {
18 | const Component = icons[id];
19 |
20 | if (!Component) {
21 | throw new Error(`No icon found for ID: ${id}`);
22 | }
23 |
24 | return (
25 |
32 |
33 |
34 | );
35 | };
36 |
37 | const Wrapper = styled.div`
38 | width: var(--size);
39 | height: var(--size);
40 |
41 | /*
42 | OMG I'm doing that thing I've warned against doing!
43 | Unfortunately, react-feather doesn't make it possible to pass
44 | discrete styles to the nested SVG within its components.
45 |
46 | Because I'm "reaching in" to third-party code, though, it feels
47 | OK. In my mind, this Icon is my bridge to that third-party code,
48 | and if I have to do some hacky stuff, that's fine. It's
49 | a special circumstance, and I won't ever have to look at the
50 | react-feather JSX and try to work out where this SVG style
51 | is coming from.
52 | */
53 | & > svg {
54 | display: block;
55 | stroke-width: var(--stroke-width);
56 | }
57 | `;
58 |
59 | export default Icon;
60 |
--------------------------------------------------------------------------------
/.storybook/global.css:
--------------------------------------------------------------------------------
1 | *,
2 | *:before,
3 | *:after {
4 | box-sizing: border-box;
5 | font-family: 'Roboto', sans-serif;
6 | }
7 |
8 | /* http://meyerweb.com/eric/tools/css/reset/
9 | v2.0 | 20110126
10 | License: none (public domain)
11 | */
12 |
13 | html,
14 | body,
15 | div,
16 | span,
17 | applet,
18 | object,
19 | iframe,
20 | h1,
21 | h2,
22 | h3,
23 | h4,
24 | h5,
25 | h6,
26 | p,
27 | blockquote,
28 | pre,
29 | a,
30 | abbr,
31 | acronym,
32 | address,
33 | big,
34 | cite,
35 | code,
36 | del,
37 | dfn,
38 | em,
39 | img,
40 | ins,
41 | kbd,
42 | q,
43 | s,
44 | samp,
45 | small,
46 | strike,
47 | strong,
48 | sub,
49 | sup,
50 | tt,
51 | var,
52 | b,
53 | u,
54 | i,
55 | center,
56 | dl,
57 | dt,
58 | dd,
59 | ol,
60 | ul,
61 | li,
62 | fieldset,
63 | form,
64 | label,
65 | legend,
66 | table,
67 | caption,
68 | tbody,
69 | tfoot,
70 | thead,
71 | tr,
72 | th,
73 | td,
74 | article,
75 | aside,
76 | canvas,
77 | details,
78 | embed,
79 | figure,
80 | figcaption,
81 | footer,
82 | header,
83 | hgroup,
84 | menu,
85 | nav,
86 | output,
87 | ruby,
88 | section,
89 | summary,
90 | time,
91 | mark,
92 | audio,
93 | video {
94 | margin: 0;
95 | padding: 0;
96 | border: 0;
97 | font-size: 100%;
98 | vertical-align: baseline;
99 | }
100 | /* HTML5 display-role reset for older browsers */
101 | article,
102 | aside,
103 | details,
104 | figcaption,
105 | figure,
106 | footer,
107 | header,
108 | hgroup,
109 | menu,
110 | nav,
111 | section {
112 | display: block;
113 | }
114 | body {
115 | line-height: 1;
116 | }
117 | ol,
118 | ul {
119 | list-style: none;
120 | }
121 | blockquote,
122 | q {
123 | quotes: none;
124 | }
125 | blockquote:before,
126 | blockquote:after,
127 | q:before,
128 | q:after {
129 | content: '';
130 | content: none;
131 | }
132 | table {
133 | border-collapse: collapse;
134 | border-spacing: 0;
135 | }
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Component Library Workshop
2 |
3 | In this workshop, we'll build 3 components from scratch:
4 |
5 | 1. ProgressBar
6 | 2. Select
7 | 3. IconInput
8 |
9 | Most of the pertinent information will be stored in the Figma document (https://www.figma.com/file/u0wCdLXheiN9f2FmAuPsE9/Mini-Component-Library), but this README will contain some additional information to help you on your mission!
10 |
11 | Two fully-formed components have already been included, to be used as-needed in your work:
12 |
13 | - `Icon`, an icon component that uses `react-feather` to render various icons
14 | - `VisuallyHidden`, a component that allows us to make text available to screen-reader users, but not to sighted users.
15 |
16 | Additionally, all of the colors you'll need are indexed in `constants.js`.
17 |
18 | All components in this project use [the `Roboto` font](https://fonts.google.com/specimen/Roboto). This font is already included in the Storybook environment, and is already applied to all elements. It comes in two weights:
19 |
20 | - 400 (default)
21 | - 700 (bold)
22 |
23 | ## Running Storybook
24 |
25 | This project uses Storybook, a component development tool.
26 |
27 | First, install dependencies with `npm install`.
28 |
29 | Once dependencies are installed, you can start storybook by running:
30 |
31 | ```
32 | npm run start
33 | ```
34 |
35 | Once running, you can visit storybook at http://localhost:6006.
36 |
37 | ## Troubleshooting
38 |
39 | ### Using Node LTS
40 |
41 | If you run into any issues running a dev server, my first suggestion would be to make sure you’re using the current LTS (Long Term Support) version of Node. There tends to be lots of bugs and quirks with the very latest release of Node.js.
42 |
43 | For example, Node 25.2 introduced a [regression](https://github.com/nodejs/node/issues/60704) that leads to an error, and this can be fixed by downgrading to Node 24.11.1 (the current LTS version, at the time of writing).
44 |
45 | ### Digital Envelope Routines
46 |
47 | You may get an error when running the `start` script that looks like this:
48 |
49 | ```
50 | Error: error:0308010C:digital envelope routines::unsupported
51 | at new Hash (node:internal/crypto/hash:67:19)
52 | at Object.createHash (node:crypto:130:10)
53 | ```
54 |
55 | You can fix this issue either by downgrading to Node 16, or by updating the `package.json` file as follows:
56 |
57 | ```diff
58 | "scripts": {
59 | - "start": "start-storybook -p 6006 -s public",
60 | + "start": "NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6006 -s public",
61 | },
62 | ```
63 |
64 | For more info, check out the [Troubleshooting Guide](https://courses.joshwcomeau.com/troubleshooting) on the course platform.
65 |
66 | ## The Components
67 |
68 | ### ProgressBar
69 |
70 | The figma document mentions that this component should be "accessible". You can learn how to build a semantically-valid, accessible progress-bar component by reading this doc: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_progressbar_role
71 |
72 | This component uses a **box shadow**. We haven't seen this property yet! For now, you can achieve this effect by copying the following CSS declaration into your component:
73 |
74 | ```css
75 | box-shadow: inset 0px 2px 4px ${COLORS.transparentGray35};
76 | ```
77 |
78 | We'll learn much more about the `box-shadow` property in future modules =)
79 |
80 | ### Select
81 |
82 | The Select component will need a down-arrow icon! You can use the `chevron-down` ID with the `Icon` component.
83 |
84 | We want to use a native `