refactor: remove component library as internal dependency (#54322)

This commit is contained in:
Sem Bauke
2024-04-08 19:35:30 +02:00
committed by GitHub
parent a22e7495ad
commit 84b1a402a8
129 changed files with 80 additions and 8824 deletions
-2
View File
@@ -9,6 +9,4 @@ shared/config/donation-settings.js
shared/config/superblocks.js
web/**
docs/**/*.md
tools/ui-components/dist/**
tools/ui-components/types/**
playwright.config.ts
+1 -5
View File
@@ -69,7 +69,6 @@
"./tsconfig.json",
"./api/tsconfig.json",
"./shared/tsconfig.json",
"./tools/ui-components/tsconfig.json",
"./tools/client-plugins/browser-scripts/tsconfig.json",
"./web/tsconfig.json",
"./curriculum-server/tsconfig.json",
@@ -90,10 +89,7 @@
}
},
{
"files": [
"./tools/ui-components/**/*.test.[jt]s?(x)",
"./client/**/*.test.[jt]s?(x)"
],
"files": ["./client/**/*.test.[jt]s?(x)"],
"extends": [
"plugin:testing-library/react",
"plugin:jest-dom/recommended"
-3
View File
@@ -210,9 +210,6 @@ api-server/lib/*
curriculum/dist
curriculum/build
### UI Components ###
tools/ui-components/dist
### Playwright ###
/playwright
-2
View File
@@ -19,6 +19,4 @@ shared/utils/get-lines.test.js
shared/utils/is-audited.js
shared/utils/validate.js
shared/utils/validate.test.js
tools/ui-components/dist
tools/ui-components/types
web/.next
+1 -1
View File
@@ -51,7 +51,7 @@
"@freecodecamp/loop-protect": "3.0.0",
"@freecodecamp/react-calendar-heatmap": "1.1.0",
"@freecodecamp/strip-comments": "3.0.1",
"@freecodecamp/ui": "workspace:*",
"@freecodecamp/ui": "0.1.0",
"@growthbook/growthbook-react": "0.20.0",
"@loadable/component": "5.16.3",
"@reach/router": "1.3.4",
-1
View File
@@ -25,7 +25,6 @@
"client/plugins/*": {
"entry": "gatsby-node.js"
},
// "tools/ui-components": {},
"tools/scripts/build": {
"entry": ["*.ts"]
}
+1 -3
View File
@@ -64,7 +64,7 @@
"lint": "npm-run-all create:shared -p lint:*",
"lint:challenges": "cd ./curriculum && pnpm run lint",
"lint:js": "eslint --cache --max-warnings 0 .",
"lint:ts": "tsc && tsc -p shared && tsc -p tools/ui-components && tsc -p api",
"lint:ts": "tsc && tsc -p shared && tsc -p api",
"lint:prettier": "prettier --list-different .",
"reload:server": "pm2 reload api-server/ecosystem.config.js",
"preseed": "npm-run-all create:shared",
@@ -80,7 +80,6 @@
"start": "npm-run-all create:shared -p develop:server serve:client",
"start-ci": "npm-run-all create:shared -p start:server serve:client-ci",
"start:server": "pm2 start api-server/ecosystem.config.js",
"storybook": "cd ./tools/ui-components && pnpm run storybook",
"test": "run-s create:shared build:curriculum build-workers test:*",
"test:source": "jest",
"test:api": "cd api && jest --force-exit",
@@ -92,7 +91,6 @@
"test-server": "jest api-server",
"test-tools": "jest tools",
"test-utils": "jest utils",
"test-ui-components": "jest tools/ui-components",
"postinstall": "cypress cache prune",
"prepare": "husky"
},
+77 -3612
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -13,4 +13,3 @@ packages:
- 'tools/scripts/build'
- 'tools/scripts/seed'
- 'tools/scripts/seed-exams'
- 'tools/ui-components'
-15
View File
@@ -1,15 +0,0 @@
module.exports = {
presets: [
'@babel/preset-react',
'@babel/preset-typescript',
[
'@babel/preset-env',
{
targets: {
browsers: ['>0.25%', 'not dead']
}
}
]
],
plugins: [['transform-react-remove-prop-types', { removeImport: true }]]
};
-2
View File
@@ -1,2 +0,0 @@
types/
dist/
-59
View File
@@ -1,59 +0,0 @@
module.exports = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.tsx'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-mdx-gfm',
'@storybook/addon-styling-webpack',
{
name: '@storybook/addon-styling-webpack',
options: {
rules: [
{
test: /\.css$/,
sideEffects: true,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1
}
},
{
loader: require.resolve('postcss-loader'),
options: {
implementation: require.resolve('postcss')
}
}
]
}
]
}
},
'@storybook/addon-webpack5-compiler-babel'
],
typescript: {
check: false,
checkOptions: {},
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: prop =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true
}
},
framework: {
name: '@storybook/react-webpack5',
options: {}
},
docs: {
autodocs: true
}
};
@@ -1,6 +0,0 @@
import { addons } from '@storybook/manager-api';
import theme from './theme';
addons.setConfig({
theme
});
-53
View File
@@ -1,53 +0,0 @@
/* the styled-elements and normalized are included here to replicate the presets that exist in the learn app */
import React from 'react';
import '../src/normalize.css';
import '../src/global-element-styles.css';
import '../src/base.css';
export const parameters = {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/
}
},
backgrounds: {
default: 'light-palette',
values: [
{
name: 'light-palette',
value: '#ffffff'
},
{
name: 'dark-palette',
value: '#0a0a23'
}
]
}
};
export const decorators = [renderTheme];
/**
* Gets matching theme name for currently selected background and provides it
* to the story.
*/
function renderTheme(Story, context) {
const selectedBackgroundValue = context.globals.backgrounds?.value;
const selectedBackgroundName = parameters.backgrounds.values.find(
bg => bg.value === selectedBackgroundValue
)?.name;
// Use the value of the default background to prevent "undefined" className
const className = selectedBackgroundName || parameters.backgrounds.default;
if (className === 'light-palette') {
document.body.classList.remove('dark-palette');
document.body.classList.add('light-palette');
} else {
document.body.classList.remove('light-palette');
document.body.classList.add('dark-palette');
}
return <Story />;
}
-8
View File
@@ -1,8 +0,0 @@
import { create } from '@storybook/theming';
export default create({
base: 'light',
brandTitle: 'freeCodeCamp.org',
brandImage:
'https://cdn.freecodecamp.org/platform/universal/fcc_secondary.svg'
});
-92
View File
@@ -1,92 +0,0 @@
{
"name": "@freecodecamp/ui",
"version": "0.0.1",
"author": "freeCodeCamp <team@freecodecamp.org>",
"license": "BSD-3-Clause",
"description": "The freeCodeCamp.org open-source UI components",
"main": "dist/bundle.js",
"module": "dist/bundle.es.js",
"style": "dist/base.css",
"types": "types/src/index.d.ts",
"files": [
"dist"
],
"private": false,
"engines": {
"node": ">=16",
"pnpm": "8"
},
"repository": {
"type": "git",
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
},
"bugs": {
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
},
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
"dependencies": {
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/react-fontawesome": "0.2.0",
"@headlessui/react": "1.7.18",
"@radix-ui/react-tabs": "1.0.3",
"react": "16.14.0",
"react-dom": "16.14.0",
"tslib": "2.6.2",
"typescript": "5.2.2"
},
"devDependencies": {
"@babel/core": "7.23.7",
"@babel/preset-env": "7.23.7",
"@babel/preset-typescript": "7.23.3",
"@rollup/plugin-babel": "5.3.1",
"@rollup/plugin-commonjs": "19.0.2",
"@rollup/plugin-node-resolve": "13.3.0",
"@rollup/plugin-typescript": "8.5.0",
"@storybook/addon-a11y": "8.0.0-beta.3",
"@storybook/addon-actions": "8.0.0-beta.3",
"@storybook/addon-docs": "8.0.0-beta.3",
"@storybook/addon-essentials": "8.0.0-beta.3",
"@storybook/addon-links": "8.0.0-beta.3",
"@storybook/addon-mdx-gfm": "8.0.0-beta.3",
"@storybook/addon-styling-webpack": "1.0.0-next.1",
"@storybook/addon-webpack5-compiler-babel": "2.0.0",
"@storybook/addon-webpack5-compiler-swc": "1.0.0",
"@storybook/blocks": "8.0.0-beta.3",
"@storybook/react": "8.0.0-beta.3",
"@storybook/react-webpack5": "8.0.0-beta.3",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "12.1.5",
"@types/jest": "29.5.12",
"@types/react": "16.14.56",
"@types/react-dom": "16.9.17",
"@types/testing-library__jest-dom": "5",
"autoprefixer": "10.4.17",
"babel-loader": "8.3.0",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"cross-env": "7.0.3",
"css-loader": "6.8.1",
"npm-run-all2": "5.0.2",
"postcss": "8.4.35",
"postcss-import": "14.1.0",
"postcss-loader": "8.1.0",
"rollup": "2.79.1",
"rollup-plugin-postcss": "4.0.2",
"rollup-plugin-terser": "7.0.2",
"storybook": "8.0.0-beta.3",
"style-loader": "3.3.3",
"tailwindcss": "3.4.1"
},
"scripts": {
"storybook": "storybook dev -p 6006",
"storybook:theming": "pnpm run storybook --no-manager-cache",
"build-storybook": "storybook build",
"build": "pnpm run build:css && pnpm run build:js && tsc",
"build:js": "cross-env NODE_ENV=production rollup -c",
"build:css": "npx -y tailwindcss -i ./src/base.css -o ./dist/base.css --minify",
"dev:js": "cross-env NODE_ENV=development rollup -c -w ",
"dev:css": "pnpm tailwindcss -i ./src/base.css -o ./dist/base.css --watch",
"develop": "npm-run-all --parallel dev:css dev:js storybook",
"clean": "rm -rf dist/*",
"gen-component": "ts-node ./utils/gen-component-script"
}
}
-7
View File
@@ -1,7 +0,0 @@
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss'),
require('autoprefixer')
]
};
-35
View File
@@ -1,35 +0,0 @@
import babel from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import postcss from 'rollup-plugin-postcss';
import { terser } from 'rollup-plugin-terser';
import resolve from '@rollup/plugin-node-resolve';
const production = process.env.NODE_ENV !== 'development';
const config = {
input: 'src/index.ts',
output: [
{
file: 'dist/bundle.js',
format: 'cjs',
sourcemap: true
},
{
file: 'dist/bundle.es.js',
format: 'es',
sourcemap: true
}
],
plugins: [
postcss(),
resolve(),
typescript({ sourceMap: true, declaration: true }),
babel({ babelHelpers: 'bundled' }),
commonjs(),
production && terser()
],
external: ['react']
};
export default config;
@@ -1,75 +0,0 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { Alert } from './alert';
const story = {
title: 'Example/Alert',
component: Alert,
argTypes: {
children: { control: { type: 'text' } },
className: { control: { type: 'text' } }
}
} satisfies Meta<typeof Alert>;
type Story = StoryObj<typeof Alert>;
export const Success: Story = {
args: {
children: 'Hello, Alert!',
variant: 'success'
}
};
export const Info: Story = {
args: {
children: 'Hello, Alert!',
variant: 'info'
}
};
export const Warning: Story = {
args: {
children: 'Hello, Alert!',
variant: 'warning'
}
};
export const Danger: Story = {
args: {
children: 'Hello, Alert!',
variant: 'danger'
}
};
export const LongText: Story = {
args: {
variant: 'success',
children:
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet animi commodi cumque dicta ducimus eum iure, maiores mollitia, odit porro quas quod rerum soluta sunt tempora unde, vel voluptas voluptates.'
}
};
export const WithHeadingAndParagraphs: Story = {
args: {
variant: 'info',
children: (
<>
<h4>
<strong>Some Heading Text</strong>
</h4>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet animi
commodi cumque dicta ducimus eum iure, maiores mollitia, odit porro
quas quod rerum soluta sunt tempora unde, vel voluptas voluptates.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet animi
commodi cumque dicta ducimus eum iure, maiores mollitia, odit porro
quas quod rerum soluta sunt tempora unde, vel voluptas voluptates.
</p>
</>
)
}
};
export default story;
@@ -1,33 +0,0 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Alert } from './alert';
describe('<Alert>', () => {
it('should have an "alert" role', () => {
render(<Alert variant='info'>Hello</Alert>);
expect(screen.getByRole('alert')).toBeInTheDocument();
});
it('renders children', () => {
const expectedText = 'Hello';
render(
<Alert variant='info'>
<p>{expectedText}</p>
</Alert>
);
expect(screen.getByText(expectedText)).toBeInTheDocument();
});
it('appends className', () => {
const expectedClass = 'basic';
render(
<Alert className={expectedClass} variant='info'>
Hello
</Alert>
);
expect(screen.getByRole('alert')).toHaveClass(expectedClass);
});
});
-40
View File
@@ -1,40 +0,0 @@
import React from 'react';
type AlertVariant = 'success' | 'info' | 'warning' | 'danger';
export type AlertProps = React.ComponentProps<'div'> & {
variant: AlertVariant;
};
const variantClasses = {
success: 'text-green-700 bg-green-50 border-green-100',
info: 'text-blue-700 bg-blue-50 border-blue-100',
warning: 'text-yellow-700 bg-yellow-50 border-yellow-100',
danger: 'text-red-700 bg-red-50 border-red-100'
};
/**
* `Alert` is used to display a short, important message that does not interrupt the user's workflow.
*
* `Alert` is not dismissable.
*/
export const Alert = ({
children,
className,
variant,
...props
}: AlertProps): JSX.Element => {
const variantClass = variantClasses[variant];
const classes = [
'p-4 mb-6 border border-solid border-1 break-words',
variantClass,
className
].join(' ');
return (
<div className={classes} role='alert' {...props}>
{children}
</div>
);
};
-2
View File
@@ -1,2 +0,0 @@
export { Alert } from './alert';
export type { AlertProps } from './alert';
@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.3 KiB

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-1
View File
@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.1 KiB

-1
View File
@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.5 KiB

-19
View File
@@ -1,19 +0,0 @@
@import './colors.css';
@tailwind base;
@layer base {
/* Override Tailwind's default `text-decoration` rule. */
/* https://github.com/tailwindlabs/tailwindcss/blob/master/src/css/preflight.css#L85 */
a {
@apply underline;
}
/* Override Tailwind's default `-webkit-tap-highlight-color` rule. */
/* https://github.com/tailwindlabs/tailwindcss/discussions/2984 */
button {
-webkit-tap-highlight-color: transparent;
}
}
@tailwind components;
@tailwind utilities;
@@ -1,118 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { Button } from '.';
const story = {
title: 'Example/Button',
component: Button,
parameters: {
controls: {
include: [
'children',
'variant',
'size',
'disabled',
'block',
'href',
'download',
'target',
'onClick'
]
}
},
argTypes: {
variant: {
options: ['primary', 'danger', 'info']
},
size: {
options: ['small', 'medium', 'large']
},
disabled: {
options: [true, false],
control: { type: 'radio' }
},
block: {
options: [true, false],
control: { type: 'radio' }
},
target: {
options: ['_self', '_blank', '_parent', '_top']
},
onClick: {
action: 'clicked'
},
href: {
control: { type: 'text' }
},
download: {
control: { type: 'text' }
}
}
} satisfies Meta<typeof Button>;
type Story = StoryObj<typeof Button>;
export const Default: Story = {
args: {
children: 'Button'
}
};
export const Danger: Story = {
args: {
variant: 'danger',
children: 'Button'
}
};
export const Info: Story = {
args: {
variant: 'info',
children: 'Button'
}
};
export const Large: Story = {
args: {
size: 'large',
children: 'Button'
}
};
export const Small: Story = {
args: {
size: 'small',
children: 'Button'
}
};
export const Disabled: Story = {
args: {
children: 'Button',
disabled: true
}
};
export const FullWidth: Story = {
args: {
children: 'Button',
block: true
}
};
export const AsALink: Story = {
args: {
children: "I'm a link that looks like a button",
href: 'https://www.freecodecamp.org'
}
};
export const AsADownloadLink: Story = {
args: {
children: "I'm a download link",
href: 'https://www.freecodecamp.org',
download: 'my_file.txt'
}
};
export default story;
@@ -1,117 +0,0 @@
// Silence the `jest-dom/prefer-enabled-disabled` rule as the rule looks for the `disabled` attribute
// while the Button component doesn't use it.
/* eslint-disable jest-dom/prefer-enabled-disabled */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Button } from './button';
describe('<Button />', () => {
it("should have the role 'button' and render the correct text", () => {
render(<Button>Hello world</Button>);
expect(
screen.getByRole('button', { name: /hello world/i })
).toBeInTheDocument();
});
it("should have the type 'button' by default", () => {
render(<Button>Hello world</Button>);
expect(
screen.getByRole('button', { name: /hello world/i })
).toHaveAttribute('type', 'button');
});
it("should have the type 'submit' if it is specified", () => {
render(<Button type='submit'>Hello world</Button>);
expect(
screen.getByRole('button', { name: /hello world/i })
).toHaveAttribute('type', 'submit');
});
it('should trigger the onClick prop on click if the component is a button element', async () => {
const onClick = jest.fn();
render(<Button onClick={onClick}>Hello world</Button>);
const button = screen.getByRole('button', { name: /hello world/i });
await userEvent.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});
it('should reflect the disabled state using the aria-disabled attribute', () => {
render(<Button disabled>Hello world</Button>);
const button = screen.getByRole('button', { name: /hello world/i });
expect(button).toHaveAttribute('aria-disabled', 'true');
// Ensure that the `disabled` attribute is not used.
expect(button).not.toHaveAttribute('disabled', 'true');
});
it('should not trigger the onClick prop if the button is disabled', async () => {
const onClick = jest.fn();
render(
<Button disabled onClick={onClick}>
Hello world
</Button>
);
const button = screen.getByRole('button', { name: /hello world/i });
await userEvent.click(button);
expect(onClick).not.toHaveBeenCalled();
});
it('should render an anchor element if the `href` prop is defined', () => {
render(<Button href='https://www.freecodecamp.org'>freeCodeCamp</Button>);
const link = screen.getByRole('link', { name: /freeCodeCamp/i });
const button = screen.queryByRole('button', { name: /freeCodeCamp/i });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', 'https://www.freecodecamp.org');
// Ensure that a button element is not rendered
expect(button).not.toBeInTheDocument();
});
it('should render a button element if the `href` and `disabled` props are both defined', () => {
render(
<Button href='https://www.freecodecamp.org' disabled>
freeCodeCamp
</Button>
);
const button = screen.getByRole('button', { name: /freeCodeCamp/i });
const link = screen.queryByRole('link', { name: /freeCodeCamp/i });
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('aria-disabled', 'true');
// Ensure that a link element is not rendered
expect(link).not.toBeInTheDocument();
});
it('should trigger the onClick prop on click if the component is an anchor element', async () => {
const onClick = jest.fn();
render(
<Button href='https://www.freecodecamp.org' onClick={onClick}>
freeCodeCamp
</Button>
);
const link = screen.getByRole('link', { name: /freeCodeCamp/i });
await userEvent.click(link);
expect(onClick).toHaveBeenCalledTimes(1);
});
});
-250
View File
@@ -1,250 +0,0 @@
import React, { useMemo } from 'react';
import { ButtonProps, ButtonSize, ButtonVariant } from './types';
const defaultClassNames = [
// Positioning
'relative',
'inline-block',
'mt-[0.5px]',
// Border
'border-solid',
'border-3',
// Active state
'active:before:w-full',
'active:before:h-full',
'active:before:absolute',
'active:before:inset-0',
'active:before:border-3',
'active:before:border-transparent',
'active:before:bg-gray-900',
'active:before:opacity-20',
// Disabled state
'aria-disabled:cursor-not-allowed',
'aria-disabled:opacity-50',
// Focus state
'focus:outline-none', // Hide the default browser outline
'focus-visible:ring',
'focus-visible:ring-focus-outline-color',
// Misc
'text-center',
'cursor-pointer',
'no-underline' // For link
];
const computeClassNames = ({
size,
variant,
disabled,
block
}: {
size?: ButtonSize;
variant?: ButtonVariant;
disabled?: boolean;
block?: boolean;
}) => {
const classNames = [...defaultClassNames];
if (block) {
classNames.push('block', 'w-full');
}
switch (variant) {
case 'danger':
classNames.push(
'border-foreground-danger',
'bg-background-danger',
'text-foreground-danger',
...(disabled
? ['active:before:hidden']
: [
'hover:bg-foreground-danger',
'hover:text-background-danger',
// This hover rule is redundant for the component library,
// but is needed to override the border color set in client's `global.css`.
// We can remove it once we have completely removed the CSS overrides in client.
'hover:border-foreground-danger',
'dark:hover:bg-background-danger',
'dark:hover:text-foreground-danger'
])
);
break;
case 'info':
classNames.push(
'border-foreground-info',
'bg-background-info',
'text-foreground-info',
...(disabled
? ['active:before:hidden']
: [
'hover:bg-foreground-info',
'hover:text-background-info',
// This hover rule is redundant for the component library,
// but is needed to override the border color set in client's `global.css`.
// We can remove it once we have completely removed the CSS overrides in client.
'hover:border-foreground-info',
'dark:hover:bg-background-info',
'dark:hover:text-foreground-info'
])
);
break;
// default variant is 'primary'
default:
classNames.push(
'border-foreground-secondary',
'bg-background-quaternary',
'text-foreground-secondary',
...(disabled
? ['active:before:hidden']
: [
'hover:bg-foreground-primary',
'hover:text-background-primary',
// This hover rule is redundant for the component library,
// but is needed to override the border color set in client's `global.css`.
// We can remove it once we have completely removed the CSS overrides in client.
'hover:border-foreground-secondary',
'dark:hover:bg-background-primary',
'dark:hover:text-foreground-primary'
])
);
}
switch (size) {
case 'large':
classNames.push('px-4 py-2.5 text-lg');
break;
case 'small':
classNames.push('px-2.5 py-1 text-sm');
break;
// default size is 'medium'
default:
classNames.push('px-3 py-1.5 text-md');
}
return classNames.join(' ');
};
const StylessButton = React.forwardRef<React.ElementRef<'button'>, ButtonProps>(
(
{ className, onClick, disabled, children, type = 'button', ...rest },
ref
) => {
// Manually prevent the click event if the button is disabled
// as `aria-disabled` marks the element disabled but still registers the click event.
// Ref: https://css-tricks.com/making-disabled-buttons-more-inclusive/#aa-the-difference-between-disabled-and-aria-disabled
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (disabled) {
return;
}
if (onClick) {
onClick(event);
}
};
return (
<button
className={className}
onClick={handleClick}
aria-disabled={disabled}
ref={ref}
type={type}
{...rest}
>
{children}
</button>
);
}
);
const Link = React.forwardRef<React.ElementRef<'a'>, ButtonProps>(
({ className, href, download, target, children, ...rest }, ref) => {
return (
<a
className={className}
download={download}
target={target}
ref={ref}
href={href}
{...rest}
>
{children}
</a>
);
}
);
export const HeadlessButton = React.forwardRef<
React.ElementRef<'button' | 'a'>,
ButtonProps
>(
(
{ onClick, className, children, disabled, href, download, target, ...rest },
ref
) => {
if (href && !disabled) {
return (
<Link
className={className}
href={href}
download={download}
target={target}
ref={ref as React.Ref<HTMLAnchorElement>}
onClick={onClick}
{...rest}
>
{children}
</Link>
);
} else {
return (
<StylessButton
className={className}
onClick={onClick}
disabled={disabled}
ref={ref as React.Ref<HTMLButtonElement>}
{...rest}
>
{children}
</StylessButton>
);
}
}
);
export const Button = React.forwardRef<
React.ElementRef<'button' | 'a'>,
ButtonProps
>(
(
{
className,
size = 'medium',
disabled,
variant = 'primary',
block,
...rest
},
ref
) => {
const classes = useMemo(
() => computeClassNames({ size, variant, disabled, block }),
[size, variant, disabled, block]
);
const buttonStyle = [className, classes].join(' ');
return (
<HeadlessButton
className={buttonStyle}
ref={ref}
disabled={disabled}
{...rest}
/>
);
}
);
Button.displayName = 'Button';
HeadlessButton.displayName = 'HeadlessButton';
StylessButton.displayName = 'StylessButton';
Link.displayName = 'Link';
-2
View File
@@ -1,2 +0,0 @@
export { Button, HeadlessButton } from './button';
export type { ButtonProps } from './types';
-19
View File
@@ -1,19 +0,0 @@
import { MouseEventHandler } from 'react';
export type ButtonVariant = 'primary' | 'danger' | 'info';
export type ButtonSize = 'small' | 'medium' | 'large';
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement | HTMLAnchorElement> {
children: React.ReactNode;
variant?: ButtonVariant;
size?: ButtonSize;
onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
type?: 'submit' | 'button';
disabled?: boolean;
block?: boolean;
href?: string;
download?: string;
target?: React.HTMLAttributeAnchorTarget;
}
@@ -1,15 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { CloseButton } from './close-button';
const story = {
title: 'Example/CloseButton',
component: CloseButton
} satisfies Meta<typeof CloseButton>;
type Story = StoryObj<typeof CloseButton>;
export const Basic: Story = {
args: {}
};
export default story;
@@ -1,36 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { CloseButton } from './close-button';
describe('<CloseButton>', () => {
it('should render', () => {
render(<CloseButton onClick={jest.fn()} />);
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('should have "Close" as the default label', () => {
render(<CloseButton onClick={jest.fn()} />);
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
});
it('should set "aria-label" to "label" prop', () => {
const expectedLabel = 'Close me please';
render(<CloseButton label={expectedLabel} onClick={jest.fn()} />);
expect(
screen.getByRole('button', { name: expectedLabel })
).toBeInTheDocument();
});
it('should call "onClick" handler on button click', async () => {
const onClick = jest.fn();
render(<CloseButton onClick={onClick} />);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
});
@@ -1,51 +0,0 @@
import React from 'react';
export interface CloseButtonProps {
className?: string;
label?: string;
onClick: () => void;
}
/**
* Basic UI component for closing modals, alerts, etc.
*/
export function CloseButton({
className,
label,
onClick
}: CloseButtonProps): JSX.Element {
const classes = [
// Remove browser's default styles
'bg-transparent',
'border-none',
// Text styles
'text-lg',
'font-bold',
'text-foreground-primary',
// Focus state
'focus:opacity-100',
'focus:text-opacity-100',
'focus:outline-none', // Hide the default browser outline
'focus-visible:ring',
'focus-visible:ring-focus-outline-color',
// Hover state
'hover:opacity-100',
'hover:text-opacity-100',
// Content positioning
'flex',
'justify-center',
'items-center',
// Others
'w-[24px]',
'h-[24px]',
'opacity-50',
className
].join(' ');
return (
<button className={classes} onClick={onClick} type='button'>
<span className='sr-only'>{label || 'Close'}</span>
<span aria-hidden>×</span>
</button>
);
}
@@ -1,2 +0,0 @@
export { CloseButton } from './close-button';
export type { CloseButtonProps } from './close-button';
@@ -1,38 +0,0 @@
import React from 'react';
import { Meta, StoryFn, StoryObj } from '@storybook/react';
import { Col } from '.';
const story = {
title: 'Example/Col',
component: Col,
argTypes: {
className: { control: { type: 'text' } },
xs: { options: [8, 12, undefined] },
sm: { options: [2, 4, 6, 8, 10, 12, undefined] },
md: { options: [4, 6, 8, 10, undefined] },
lg: { options: [6, 8, 10, undefined] },
xsOffset: { options: [2, 3, undefined] },
smOffset: { options: [1, 2, 3, 4, undefined] },
mdOffset: { options: [1, 2, 3, 4, undefined] },
lgOffset: { options: [0, 1, 2, undefined] },
smPush: { options: [1, undefined] }
}
} satisfies Meta<typeof Col>;
const Template: StoryFn<typeof Col> = args => {
return (
<Col className='bg-gray-700' {...args}>
<p>Random text to test the element width</p>
</Col>
);
};
export const Default: StoryObj<typeof Col> = {
render: Template,
args: {
// default props go here
}
};
export default story;
-31
View File
@@ -1,31 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Col } from '.';
describe('<Col />', () => {
it('should change className when props are passed', () => {
render(
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
Learn to code for free.
</Col>
);
expect(screen.getByText('Learn to code for free.')).toHaveClass(
'min-h-[1px] px-[15px] w-full md:w-5/6 min-[1200px]:w-2/3 md:ml-[8.3%] min-[1200px]:ml-[16.6%]'
);
});
it('should have lgOffSet 0 when it is passed to the component', () => {
render(<Col lgOffset={0}>Learn to code for free.</Col>);
expect(screen.getByText('Learn to code for free.')).toHaveClass(
'min-h-[1px] px-[15px] min-[1200px]:ml-0'
);
});
it('should add className to it', () => {
render(
<Col className='certificate-outer-wrapper'>Learn to code for free.</Col>
);
expect(screen.getByText('Learn to code for free.')).toHaveClass(
'min-h-[1px] px-[15px] certificate-outer-wrapper'
);
});
});
-98
View File
@@ -1,98 +0,0 @@
import React from 'react';
import { ColProps } from './types';
const ExtraSmallClasses = {
6: 'w-1/2',
8: 'w-2/3',
10: 'w-5/6',
12: 'w-full'
};
const ExtraSmallOffsetClasses = {
1: 'ml-[8.3%]',
2: 'ml-[16.6%]',
3: 'ml-[25%]'
};
const SmallClasses = {
2: 'md:w-1/6',
4: 'md:w-1/3',
5: 'md:w-5/12',
8: 'md:w-2/3',
6: 'md:w-1/2',
10: 'md:w-5/6',
12: 'md:w-full'
};
const SmallOffsetClasses = {
1: 'md:ml-[8.3%]',
2: 'md:ml-[16.6%]',
3: 'md:ml-[25%]',
4: 'md:ml-[33.3%]'
};
const MediumClasses = {
4: 'min-[992px]:w-1/3',
6: 'min-[992px]:w-1/2',
8: 'min-[992px]:w-2/3',
10: 'min-[992px]:w-5/6',
12: 'min-[992px]:w-full'
};
const MediumOffsetClasses = {
1: 'min-[992px]:ml-[8.3%]',
2: 'min-[992px]:ml-[16.6%]',
3: 'min-[992px]:ml-[25%]',
4: 'min-[992px]:ml-[33.3%]'
};
const LargeClasses = {
6: 'min-[1200px]:w-1/2',
8: 'min-[1200px]:w-2/3',
10: 'min-[1200px]:w-5/6'
};
const LargeOffsetClasses = {
0: 'min-[1200px]:ml-0',
1: 'min-[1200px]:ml-[8.3%]',
2: 'min-[1200px]:ml-[16.6%]'
};
export const Col = ({
className,
children,
xs,
sm,
md,
lg,
smPush,
xsOffset,
smOffset,
mdOffset,
lgOffset,
...props
}: ColProps) => {
const xsClass = xs ? ExtraSmallClasses[xs] : '';
const xsOffsetClass = xsOffset ? ExtraSmallOffsetClasses[xsOffset] : '';
const smPushClass = smPush ? 'md:left-[8.3%]' : '';
const smClass = sm ? SmallClasses[sm] : '';
const smOffsetClass = smOffset ? SmallOffsetClasses[smOffset] : '';
const mdClass = md ? MediumClasses[md] : '';
const mdOffsetClass = mdOffset ? MediumOffsetClasses[mdOffset] : '';
const lgClass = lg ? LargeClasses[lg] : '';
// we have to check condiontionally against undefined, because "lgOffset ?" clear the 0 value, maybe refactor LargeOffsetClasses[0] later to something isn't 0.
const lgOffsetClass =
lgOffset !== undefined ? LargeOffsetClasses[lgOffset] : '';
return (
<div
className={`${
className ?? ''
} min-h-[1px] px-[15px] ${xsClass} ${smClass} ${mdClass} ${lgClass} ${xsOffsetClass} ${smOffsetClass} ${mdOffsetClass} ${lgOffsetClass} ${smPushClass} `}
{...props}
>
{children}
</div>
);
};
-2
View File
@@ -1,2 +0,0 @@
export { Col } from './col';
export type { ColProps } from './types';
-13
View File
@@ -1,13 +0,0 @@
export interface ColProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
children?: React.ReactNode;
xs?: 6 | 8 | 10 | 12;
sm?: 2 | 4 | 5 | 6 | 8 | 10 | 12;
md?: 4 | 6 | 8 | 10 | 12;
lg?: 6 | 8 | 10;
xsOffset?: 1 | 2 | 3;
smOffset?: 1 | 2 | 3 | 4;
mdOffset?: 1 | 2 | 3 | 4;
lgOffset?: 0 | 1 | 2;
smPush?: 1;
}
@@ -1,11 +0,0 @@
import React from 'react';
import { AllPalettes } from './color-system';
const story = {
title: 'Design System/Color',
component: AllPalettes
};
export const ColorSystem = (): JSX.Element => <AllPalettes />;
export default story;
@@ -1,67 +0,0 @@
import React from 'react';
import colorList from '../colors.css';
import { Color, ColorList, PaletteProps } from './types';
// ---------------------------------------------------------- //
// HELPER FUNCTIONS //
// ---------------------------------------------------------- //
/**
* Transform colorList from an object to an array of objects
* @example
* Input: { '--blue10': 'var(--blue10)' }
* Output: [{ label: 'blue10', value: 'var(--blue10)' }]
*/
const transformedColorList = Object.keys(colorList as ColorList).map(
colorName => ({
label: colorName.replace('--', ''),
value: (colorList as ColorList)[colorName]
})
);
// Get the background and text color values of each palette item
const getPaletteItemStyle = (color: Color) => {
const itemTextColor = color.label.substring(color.label.length - 2);
return {
backgroundColor: color.value,
// Extract the scale from the color label.
// If the scale is greater or equal to 50, use white text for the label; otherwise, use dark text.
color: parseInt(itemTextColor, 10) >= 50 ? '#ffffff' : '#0a0a23'
};
};
const getPaletteByColorName = (name: string) =>
transformedColorList.filter(color => color.label.includes(name));
// ---------------------------------------------------------- //
// COMPONENTS //
// ---------------------------------------------------------- //
const Palette = ({ colors }: PaletteProps) => {
return (
<div className='inline-flex flex-col m-4 w-3/12'>
{colors.map(color => (
<div
className='flex items-center p-2 h-8'
key={color.label}
style={getPaletteItemStyle(color)}
>
{color.label}
</div>
))}
</div>
);
};
export const AllPalettes = (): JSX.Element => {
return (
<>
<Palette colors={getPaletteByColorName('gray')} />
<Palette colors={getPaletteByColorName('purple')} />
<Palette colors={getPaletteByColorName('yellow')} />
<Palette colors={getPaletteByColorName('blue')} />
<Palette colors={getPaletteByColorName('green')} />
<Palette colors={getPaletteByColorName('red')} />
</>
);
};
@@ -1,10 +0,0 @@
export interface Color {
label: string;
value: string;
}
export interface PaletteProps {
colors: Color[];
}
export type ColorList = Record<string, string>;
-128
View File
@@ -1,128 +0,0 @@
:root {
--gray00: #ffffff;
--gray05: #f5f6f7;
--gray10: #dfdfe2;
--gray15: #d0d0d5;
--gray45: #858591;
--gray75: #3b3b4f;
--gray80: #2a2a40;
--gray85: #1b1b32;
--gray90: #0a0a23;
--purple10: #dbb8ff;
--purple50: #9400d3;
--purple90: #5a01a7;
--yellow05: #fcf8e3;
--yellow10: #faebcc;
--yellow40: #ffc300;
--yellow45: #ffbf00;
--yellow50: #f1be32;
--yellow70: #8a6d3b;
--yellow90: #4d3800;
--blue05: #d9edf7;
--blue10: #bce8f1;
--blue30: #99c9ff;
--blue50: #198eee;
--blue70: #31708f;
--blue90: #002ead;
--green05: #dff0d8;
--green10: #d6e9c6;
--green40: #acd157;
--green70: #3c763d;
--green90: #00471b;
--red05: #f2dede;
--red10: #ebccd1;
--red15: #ffadad;
--red30: #f8577c;
--red70: #a94442;
--red80: #f82153;
--red90: #850000;
}
/* Export the variables in order to use them on the Color System Storybook page */
:export {
--gray00: var(--gray00);
--gray05: var(--gray05);
--gray10: var(--gray10);
--gray15: var(--gray15);
--gray45: var(--gray45);
--gray75: var(--gray75);
--gray80: var(--gray80);
--gray85: var(--gray85);
--gray90: var(--gray90);
--purple10: var(--purple10);
--purple50: var(--purple50);
--purple90: var(--purple90);
--yellow05: var(--yellow05);
--yellow10: var(--yellow10);
--yellow40: var(--yellow40);
--yellow45: var(--yellow45);
--yellow50: var(--yellow50);
--yellow70: var(--yellow70);
--yellow90: var(--yellow90);
--blue05: var(--blue05);
--blue10: var(--blue10);
--blue30: var(--blue30);
--blue50: var(--blue50);
--blue70: var(--blue70);
--blue90: var(--blue90);
--green05: var(--green05);
--green10: var(--green10);
--green40: var(--green40);
--green70: var(--green70);
--green90: var(--green90);
--red05: var(--red05);
--red10: var(--red10);
--red15: var(--red15);
--red30: var(--red30);
--red70: var(--red70);
--red80: var(--red80);
--red90: var(--red90);
}
.light-palette {
--foreground-primary: var(--gray90);
--foreground-secondary: var(--gray85);
--foreground-tertiary: var(--gray80);
--foreground-quaternary: var(--gray75);
--foreground-danger: var(--red15);
--foreground-info: var(--blue30);
--foreground-warning: var(--yellow45);
--background-primary: var(--gray00);
--background-secondary: var(--gray05);
--background-tertiary: var(--gray10);
--background-quaternary: var(--gray15);
--background-danger: var(--red90);
--background-info: var(--blue90);
--focus-outline-color: var(--blue50);
}
.dark-palette {
--foreground-primary: var(--gray00);
--foreground-secondary: var(--gray05);
--foreground-tertiary: var(--gray10);
--foreground-quaternary: var(--gray15);
--foreground-danger: var(--red90);
--foreground-info: var(--blue90);
--foreground-warning: var(--yellow40);
--background-primary: var(--gray90);
--background-secondary: var(--gray85);
--background-tertiary: var(--gray80);
--background-quaternary: var(--gray75);
--background-danger: var(--red15);
--background-info: var(--blue30);
--focus-outline-color: var(--blue50);
}
@@ -1,41 +0,0 @@
import React from 'react';
import { Meta, StoryFn, StoryObj } from '@storybook/react';
import { Container } from '.';
const story = {
title: 'Example/Container',
component: Container,
argTypes: {
fluid: {
control: {
type: 'boolean'
}
}
}
} satisfies Meta<typeof Container>;
const Template: StoryFn<typeof Container> = args => {
return (
<Container {...args}>
<p>Random text to test the element width</p>
<p>Random text to test the element width</p>
<p>Random text to test the element width</p>
<p>Random text to test the element width</p>
<p>Random text to test the element width</p>
<p>Random text to test the element width</p>
<p>Random text to test the element width</p>
<p>Random text to test the element width</p>
<p>Random text to test the element width</p>
<p>Random text to test the element width</p>
<p>Random text to test the element width</p>
<p>Random text to test the element width</p>
</Container>
);
};
export const Default: StoryObj<typeof Container> = {
render: Template,
args: {}
};
export default story;
@@ -1,23 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Container } from '.';
describe('<Container />', () => {
it('remove width when the container is fluid', () => {
render(<Container fluid={true}>Learn to code for free.</Container>);
expect(screen.getByText('Learn to code for free.')).toHaveClass(
'mx-auto px-[15px] '
);
});
it('should add className to it', () => {
render(
<Container className='certificate-outer-wrapper'>
Learn to code for free.
</Container>
);
expect(screen.getByText('Learn to code for free.')).toHaveClass(
'mx-auto px-[15px] my-0 md:w-[750px] min-[992px]:w-[970px] min-[1200px]:w-[1170px] certificate-outer-wrapper'
);
});
});
@@ -1,18 +0,0 @@
import React from 'react';
import type { ContainerProps } from './types';
export const Container = ({
children,
className,
fluid
}: ContainerProps): JSX.Element => {
const elementClasses = fluid
? ''
: 'my-0 md:w-[750px] min-[992px]:w-[970px] min-[1200px]:w-[1170px]';
return (
<div className={`mx-auto px-[15px] ${elementClasses} ${className ?? ''}`}>
{children}
</div>
);
};
@@ -1,2 +0,0 @@
export { Container } from './container';
export type { ContainerProps } from './types';
@@ -1,5 +0,0 @@
export type ContainerProps = {
children?: React.ReactNode;
className?: string;
fluid?: boolean;
};
@@ -1,27 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { ControlLabel } from '.';
const story = {
title: 'Example/ControlLabel',
component: ControlLabel,
parameters: {
controls: {
include: ['className']
}
},
argTypes: {
className: { control: { type: 'text' } },
htmlFor: { control: { type: 'text' } },
srOnly: { options: ['srOnly', ''] }
}
} satisfies Meta<typeof ControlLabel>;
type Story = StoryObj<typeof ControlLabel>;
export const Default: Story = {
args: {
children: 'Control Label'
}
};
export default story;
@@ -1,33 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { FormGroup } from '../form-group';
import { ControlLabel } from '.';
describe('<ControlLabel>', () => {
it('should inherit `controlId` from FormGroup', () => {
render(
<FormGroup controlId='foo'>
<ControlLabel>Label</ControlLabel>
</FormGroup>
);
const labelElement = screen.getByText('Label');
expect(labelElement).toBeInTheDocument();
expect(labelElement).toHaveAttribute('for', 'foo');
});
it('should use `htmlFor` over `controlId` if both are specified', () => {
render(
<FormGroup controlId='foo'>
<ControlLabel htmlFor='bar'>Label</ControlLabel>
</FormGroup>
);
const labelElement = screen.getByText('Label');
expect(labelElement).toBeInTheDocument();
expect(labelElement).toHaveAttribute('for', 'bar');
});
});
@@ -1,33 +0,0 @@
import React, { useContext } from 'react';
import { FormContext } from '../form-group/form-group';
import { ControlLabelProps } from './types';
const validationLabel = {
success: 'text-background-info',
warning: 'text-background-warning',
error: 'text-background-danger'
};
export const ControlLabel = ({
className,
htmlFor,
srOnly,
...props
}: ControlLabelProps): JSX.Element => {
const { controlId, validationState } = useContext(FormContext);
const labelStyle = validationState
? validationLabel[validationState]
: undefined;
const screenOnlyClass = srOnly ? 'sr-only' : undefined;
const defaultClasses = [labelStyle, screenOnlyClass, className].join(' ');
return (
<label
className={defaultClasses}
htmlFor={htmlFor || controlId}
{...props}
/>
);
};
@@ -1,2 +0,0 @@
export { ControlLabel } from './control-label';
export type { ControlLabelProps } from './types';
@@ -1,7 +0,0 @@
import React from 'react';
export interface ControlLabelProps
extends React.LabelHTMLAttributes<HTMLLabelElement> {
htmlFor?: string;
srOnly?: boolean;
}
-1
View File
@@ -1 +0,0 @@
declare module '*.css';
@@ -1,78 +0,0 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { MenuItem } from './menu-item/menu-item';
import { Dropdown } from './drop-down';
const story = {
title: 'Example/Dropdown',
component: Dropdown
} satisfies Meta<typeof Dropdown>;
const DropDownChildren = () => (
<>
<Dropdown.Toggle>Options</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem onClick={() => alert('hi')}>Option 1</MenuItem>
<MenuItem href={'https://www.google.com'}>Option 2</MenuItem>
<MenuItem href={'https://www.google.com'}>Option 3</MenuItem>
</Dropdown.Menu>
</>
);
const DropUpChildren = () => (
<>
<Dropdown.Toggle>Options</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem onClick={() => alert('hi')}>Option 1</MenuItem>
<MenuItem href={'https://www.google.com'}>Option 2</MenuItem>
<MenuItem href={'https://www.google.com'} disabled>
Option 3
</MenuItem>
</Dropdown.Menu>
</>
);
export const Menus: StoryObj<typeof Dropdown> = {
decorators: [
Story => (
<div
style={{
height: '220px',
width: '220px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-start'
}}
>
{Story()}
</div>
)
],
render: () => (
<Dropdown>
<DropDownChildren />
</Dropdown>
)
};
export const DropUp: StoryObj<typeof Dropdown> = {
decorators: [
Story => (
<div
style={{
height: '220px',
width: '220px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end'
}}
>
{Story()}
</div>
)
],
args: {
children: <DropUpChildren />,
dropup: true
}
};
export default story;
@@ -1,178 +0,0 @@
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { MenuItem } from './menu-item/menu-item';
import { Dropdown } from './drop-down';
describe('<DropDownButton>', () => {
it('should render button with text', async () => {
render(
<Dropdown>
<Dropdown.Toggle>Some Button</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem onClick={() => alert('hi')}>Option</MenuItem>
<MenuItem href={'https://www.google.com'}>Option</MenuItem>
<MenuItem href={'https://www.google.com'}>Option</MenuItem>
</Dropdown.Menu>
</Dropdown>
);
const dropdownTrigger = screen.getByRole('button', { name: 'Some Button' });
await userEvent.click(dropdownTrigger);
const unorderedList = screen.getByRole('menu');
const item = within(unorderedList).getAllByRole('menuitem');
expect(unorderedList).toBeInTheDocument();
expect(dropdownTrigger).toBeInTheDocument();
expect(item.length).toBe(3);
});
it('should render button with direction to up', async () => {
render(
<Dropdown dropup={true}>
<Dropdown.Toggle>Some Button</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem onClick={() => alert('hi')}>Option 1</MenuItem>
<MenuItem href={'https://www.google.com'}>Option 2</MenuItem>
<MenuItem href={'https://www.google.com'}>Option 3</MenuItem>
</Dropdown.Menu>
</Dropdown>
);
const dropdownTrigger = screen.getByRole('button', { name: 'Some Button' });
await userEvent.click(dropdownTrigger);
const unorderedList = screen.getByRole('menu');
expect(unorderedList).toHaveClass(
'list-none bg-foreground-secondary text-center border-1 border-solid border-background-quaternary focus:outline-transparent origin-top-right absolute w-full min-w-max py-1 px-0 z-10 transform -translate-y-full top-0'
);
});
it("should have the role 'button' and render the correct text", async () => {
render(
<Dropdown>
<Dropdown.Toggle>test</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem>Hello world</MenuItem>
</Dropdown.Menu>
</Dropdown>
);
const dropDownTrigger = screen.getByRole('button', { name: 'test' });
await userEvent.click(dropDownTrigger);
const unorderedList = screen.getByRole('menu');
const item = within(unorderedList).getByRole('menuitem', {
name: 'Hello world'
});
expect(item).toBeInTheDocument();
});
it('should trigger the onClick prop on click', async () => {
const onClick = jest.fn();
render(
<Dropdown>
<Dropdown.Toggle>test</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem onClick={onClick}>Hello world</MenuItem>
</Dropdown.Menu>
</Dropdown>
);
const dropDownTrigger = screen.getByRole('button', { name: 'test' });
await userEvent.click(dropDownTrigger);
const unorderedList = screen.getByRole('menu');
const item = within(unorderedList).getByRole('menuitem');
await userEvent.click(item);
expect(onClick).toHaveBeenCalledTimes(1);
});
it('should reflect the disabled state using the aria-disabled attribute', async () => {
render(
<Dropdown>
<Dropdown.Toggle>test</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem disabled>Hello world</MenuItem>
</Dropdown.Menu>
</Dropdown>
);
const dropDownTrigger = screen.getByRole('button', { name: 'test' });
await userEvent.click(dropDownTrigger);
const unorderedList = screen.getByRole('menu');
const item = within(unorderedList).getByRole('menuitem', {
name: 'Hello world'
});
expect(item).toHaveAttribute('aria-disabled', 'true');
// Ensure that the `disabled` attribute is not used.
expect(item).toBeEnabled();
});
it('should not trigger the onClick prop if the button is disabled', async () => {
const onClick = jest.fn();
render(
<Dropdown>
<Dropdown.Toggle>test</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem disabled onClick={onClick}>
Hello world
</MenuItem>
</Dropdown.Menu>
</Dropdown>
);
const dropDownTrigger = screen.getByRole('button', { name: 'test' });
await userEvent.click(dropDownTrigger);
const unorderedList = screen.getByRole('menu');
const item = within(unorderedList).getByRole('menuitem', {
name: 'Hello world'
});
await userEvent.click(item);
expect(onClick).not.toHaveBeenCalled();
});
it('should render an anchor element if the `href` prop is defined', async () => {
render(
<Dropdown>
<Dropdown.Toggle>test</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem href='https://www.freecodecamp.org'>freeCodeCamp</MenuItem>
</Dropdown.Menu>
</Dropdown>
);
const dropDownTrigger = screen.getByRole('button', { name: 'test' });
await userEvent.click(dropDownTrigger);
const unorderedList = screen.getByRole('menu');
const item = within(unorderedList).getByRole('menuitem', {
name: 'freeCodeCamp'
});
expect(item).toBeInTheDocument();
expect(item).toHaveAttribute('href', 'https://www.freecodecamp.org');
});
it('should render a button element if the `href` and `disabled` props are both defined', async () => {
render(
<Dropdown>
<Dropdown.Toggle>test</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem href='https://www.freecodecamp.org' disabled>
freeCodeCamp
</MenuItem>
</Dropdown.Menu>
</Dropdown>
);
const dropDownTrigger = screen.getByRole('button', { name: 'test' });
await userEvent.click(dropDownTrigger);
const unorderedList = screen.getByRole('menu');
const item = within(unorderedList).getByRole('menuitem', {
name: 'freeCodeCamp'
});
expect(item).toBeInTheDocument();
expect(item).toHaveAttribute('aria-disabled', 'true');
});
});
@@ -1,116 +0,0 @@
import React, { createContext, useContext, useRef } from 'react';
import { Menu } from '@headlessui/react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCaretDown, faCaretUp } from '@fortawesome/free-solid-svg-icons';
import { Props } from '@headlessui/react/dist/types';
type MenuTag = React.ExoticComponent<{
children?: React.ReactNode;
}>;
interface MenuRenderPropArg {
open: boolean;
close: () => void;
}
type DropdownMenuProps = Props<
MenuTag,
MenuRenderPropArg,
never,
{
__demoMode?: boolean;
}
>;
export type DropdownProps = DropdownMenuProps & {
dropup?: boolean;
id?: string;
};
export interface ButtonRenderPropArg {
open: boolean;
}
type ButtonPropsWeControl = 'aria-controls' | 'aria-expanded' | 'aria-haspopup';
type DropDownButtonProps = Props<
'button',
ButtonRenderPropArg,
ButtonPropsWeControl,
{
disabled?: boolean;
}
>;
type DropDownContextProps = DropdownProps & {
menuButtonRef: React.MutableRefObject<HTMLButtonElement | null>;
};
const DropDownContext = createContext<DropDownContextProps>({
menuButtonRef: React.createRef()
});
const dropDownItems =
'list-none bg-foreground-secondary text-center border-1 border-solid border-background-quaternary focus:outline-transparent origin-top-right absolute w-full min-w-max py-1 px-0 z-10';
const dropUpItems = dropDownItems + ' transform -translate-y-full top-0';
const toggleClassNames =
'cursor-pointer border-3 border-solid w-full block text-center touch-manipulation bg-background-quaternary text-foreground-secondary px-3 py-1.5 relative hover:bg-foreground-secondary hover:text-background-secondary btn-block border-foreground-secondary';
export const MenuItems = React.forwardRef<HTMLDivElement, DropDownButtonProps>(
({ children, className }, ref): JSX.Element => {
const { dropup, menuButtonRef } = useContext(DropDownContext);
const handleClick = () => {
menuButtonRef.current?.focus();
};
const itemsClasses = dropup ? dropUpItems : dropDownItems;
const buttonClass: string = [className, itemsClasses].join(' ');
return (
<Menu.Items className={buttonClass} ref={ref} onClick={handleClick}>
{children}
</Menu.Items>
);
}
);
const DropDownButton = ({
children,
className,
...rest
}: DropDownButtonProps): JSX.Element => {
const { dropup, menuButtonRef } = useContext(DropDownContext);
const classes = [className, toggleClassNames].join(' ');
return (
<Menu.Button ref={menuButtonRef} className={classes} {...rest}>
{children}
<FontAwesomeIcon
icon={dropup ? faCaretUp : faCaretDown}
className='mx-2 h-3'
aria-hidden='true'
/>
</Menu.Button>
);
};
export const Dropdown = ({
children,
dropup,
...props
}: DropdownProps): JSX.Element => {
const menuButtonRef = useRef(null);
const context = { dropup, menuButtonRef };
return (
<DropDownContext.Provider value={context}>
<Menu className='relative w-full' as='div' {...props}>
{children}
</Menu>
</DropDownContext.Provider>
);
};
Dropdown.Menu = MenuItems;
Dropdown.Toggle = DropDownButton;
MenuItems.displayName = 'MenuItems';
Dropdown.displayName = 'Dropdown';
@@ -1 +0,0 @@
export { Dropdown, type DropdownProps } from './drop-down';
@@ -1 +0,0 @@
export { MenuItem, type MenuItemsProps } from './menu-item';
@@ -1,31 +0,0 @@
import { Menu } from '@headlessui/react';
import React from 'react';
import { type ButtonProps, HeadlessButton } from '../../button';
export type MenuItemsProps = React.ComponentPropsWithoutRef<typeof Menu.Items> &
ButtonProps;
const defaultClass =
'block text-center no-underline border-none px-4 py-1.5 focus:bg-background-secondary focus:text-foreground-secondary hover:text-foreground-secondary hover:bg-background-secondary w-full';
export const MenuItem = ({
children,
className,
...props
}: MenuItemsProps): JSX.Element => {
return (
<Menu.Item>
{({ active }) => {
const activeStyles = active
? 'text-foreground-secondary bg-background-secondary outline outline-3 outline-blue-500 outline-offset-[-3px]'
: 'text-background-secondary bg-foreground-secondary';
const classes = [defaultClass, className, activeStyles].join(' ');
return (
<HeadlessButton className={classes} {...props}>
{children}
</HeadlessButton>
);
}}
</Menu.Item>
);
};
@@ -1,17 +0,0 @@
import React from 'react';
export const FormControlFeedback = ({
children,
className,
...props
}: React.ComponentProps<'span'>): JSX.Element => {
const defaultClasses =
'absolute top-[30px] right-0 z-2 block w-8 h-8 leading-8 text-center pointer-events-none text-green-700';
const classes = [className, defaultClasses].join(' ');
return (
<span className={classes} {...props}>
{children}
</span>
);
};
@@ -1,16 +0,0 @@
import React from 'react';
export const FormControlStatic = ({
className,
children,
...props
}: React.ComponentProps<'p'>): JSX.Element => {
const defaultClasses = 'py-1.5 mb-0 min-h-43-px text-foreground-secondary';
const classes = [defaultClasses, className].join(' ');
return (
<p className={classes} {...props}>
{children}
</p>
);
};
@@ -1,80 +0,0 @@
import React from 'react';
import { Meta, StoryFn, StoryObj } from '@storybook/react';
import { FormControl } from '.';
const story = {
title: 'Example/FormControl',
component: FormControl,
parameters: {
controls: {
include: [
'className',
'id',
'onChange',
'value',
'componentClass',
'placeholder',
'required',
'type'
]
}
},
argTypes: {
className: { control: { type: 'text' } },
id: { control: { type: 'text' } },
onChange: { action: 'changed' },
value: { control: { type: 'text' } },
componentClass: {
options: ['input', 'textarea']
},
placeholder: { control: { type: 'text' } },
required: { control: 'boolean' },
type: { options: ['text', 'email', 'url'] }
}
} satisfies Meta<typeof FormControl>;
type Story = StoryObj<typeof FormControl>;
export const Default: Story = {};
const StaticTemplate: StoryFn<(typeof FormControl)['Static']> = args => {
return <FormControl.Static {...args} />;
};
export const Static: StoryObj<typeof FormControl.Static> = {
render: StaticTemplate,
args: {
children: 'foo@bar.com'
}
};
const FeedBackTemplate: StoryFn<React.ComponentProps<'span'>> = args => {
return <FormControl.Feedback {...args} />;
};
const checkMark = (
<svg
aria-hidden='true'
focusable='false'
data-prefix='fas'
data-icon='check'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 448 512'
>
<path
fill='currentColor'
d='M438.6 105.4C451.1 117.9 451.1 138.1 438.6 150.6L182.6 406.6C170.1 419.1 149.9 419.1 137.4 406.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4C21.87 220.9 42.13 220.9 54.63 233.4L159.1 338.7L393.4 105.4C405.9 92.88 426.1 92.88 438.6 105.4H438.6z'
></path>
</svg>
);
export const Feedback: StoryObj<typeof FormControl.Feedback> = {
render: FeedBackTemplate,
args: {
children: checkMark
}
};
export default story;
@@ -1,39 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { FormGroup } from '../form-group';
import { FormControl } from '.';
describe('<FormControl />', () => {
it('should render correctly', () => {
render(<FormControl aria-label='test' />);
expect(screen.getByLabelText('test')).toBeInTheDocument();
});
it('should use `id` over `controlId` if both are specified', () => {
render(
<FormGroup controlId='foo'>
<FormControl id='bar' />
</FormGroup>
);
const input = screen.getByRole('textbox');
expect(input).toBeInTheDocument();
expect(input).toHaveAttribute('id', 'bar');
});
});
describe('<FormControl.Static />', () => {
it('should render correctly', () => {
render(<FormControl.Static aria-label='test' />);
expect(screen.getByLabelText('test')).toBeInTheDocument();
});
});
describe('<FormControl.Feedback />', () => {
it('should render correctly', () => {
render(<FormControl.Feedback aria-label='test' />);
expect(screen.getByLabelText('test')).toBeInTheDocument();
});
});
@@ -1,33 +0,0 @@
import React, { useContext } from 'react';
import { FormContext } from '../form-group/form-group';
import { FormControlFeedback } from './form-control-feedback';
import { FormControlStatic } from './form-control-static';
import { FormControlProps } from './types';
// Uses controlId from <FormGroup> if not explicitly specified.
// type Only relevant if componentClass is 'input'.
let variantClass: string;
const defaultClasses =
'outline-0 block w-full py-1.5 px-2.5 text-md text-foreground-primary bg-background-primary bg-none rounded-none border-1 border-solid border-background-quaternary shadow-none transition ease-in-out duration-150 focus:border-foreground-tertiary';
const FormControl = ({
componentClass,
...props
}: FormControlProps<'input' | 'textarea'>): JSX.Element => {
const { controlId } = useContext(FormContext);
const { id, className } = props;
const Component = componentClass || 'input';
if (Component !== 'textarea') variantClass = ' h-8';
//row and componentClass
const classes = [className, defaultClasses, variantClass].join(' ');
return <Component id={id || controlId} className={classes} {...props} />;
};
FormControl.Feedback = FormControlFeedback;
FormControl.Static = FormControlStatic;
export { FormControl };
@@ -1,2 +0,0 @@
export { FormControl } from './form-control';
export type { FormControlProps } from './types';
@@ -1,9 +0,0 @@
import React from 'react';
export type FormControlProps<
TElement extends
| keyof JSX.IntrinsicElements
| React.JSXElementConstructor<unknown> = 'input'
> = {
componentClass?: TElement | string;
} & React.ComponentProps<TElement>;
@@ -1,58 +0,0 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { FormControl } from '../form-control';
import { ControlLabel } from '../control-label';
import { FormGroup } from '.';
const story = {
title: 'Example/FormGroup',
component: FormGroup,
argTypes: {
children: { control: { type: 'object' } },
className: { control: { type: 'text' } },
controlId: { control: { type: 'text' } },
as: { control: { type: 'text' } },
validationState: { options: ['success', 'warning', 'error', null] }
}
} satisfies Meta<typeof FormGroup>;
type Story = StoryObj<typeof FormGroup>;
const Child = () => {
return (
<>
<ControlLabel>Label</ControlLabel>
<FormControl type={'text'} placeholder='Hello World' />
</>
);
};
export const Default: Story = {
args: {
children: <Child />
}
};
export const Success: Story = {
args: {
children: <Child />,
validationState: 'success'
}
};
export const Error: Story = {
args: {
children: <Child />,
validationState: 'error'
}
};
export const Warning: Story = {
args: {
children: <Child />,
validationState: 'warning'
}
};
export default story;
@@ -1,45 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { FormControl } from '../form-control/.';
import { ControlLabel } from '../control-label';
import { FormGroup } from '.';
const sameNumberOfChildren = 2;
describe('<FormGroup>', () => {
it('renders children', () => {
render(
<FormGroup data-testid='test-id'>
<span title='Child' />
<span title='Child' />
</FormGroup>
);
const element = screen.getByTestId('test-id');
expect(element.childNodes.length).toBe(sameNumberOfChildren);
const formGroupChildren = screen.getAllByTitle('Child');
expect(formGroupChildren.length).toBe(sameNumberOfChildren);
element.childNodes.forEach((child, index) => {
expect(child).toBe(formGroupChildren[index]);
});
});
it('should pass `controlId` to the label and input elements', () => {
render(
<FormGroup controlId='foo'>
<ControlLabel>Foo</ControlLabel>
<FormControl />
</FormGroup>
);
const label = screen.getByText('Foo');
const input = screen.getByLabelText('Foo');
expect(label).toBeInTheDocument();
expect(label).toHaveAttribute('for', 'foo');
expect(input).toBeInTheDocument();
expect(input).toHaveAttribute('id', 'foo');
});
});
@@ -1,37 +0,0 @@
import React, { createContext } from 'react';
import { FormGroupProps } from './types';
export type FormContextProps = Pick<
FormGroupProps,
'controlId' | 'validationState'
>;
export const FormContext = createContext<FormContextProps>({});
const defaultClasses = 'mb-3.5 relative';
export const FormGroup = ({
className,
validationState,
controlId,
as,
...props
}: FormGroupProps): JSX.Element => {
const context = {
controlId,
validationState
};
const componentClass = as;
const Component = componentClass || 'div';
const classes = [defaultClasses, className].join(' ');
return (
<FormContext.Provider value={context}>
<Component
className={classes}
as={as}
{...props}
validationstate={validationState}
/>
</FormContext.Provider>
);
};
@@ -1,2 +0,0 @@
export { FormGroup } from './form-group';
export type { FormGroupProps } from './types';
@@ -1,7 +0,0 @@
import React from 'react';
export interface FormGroupProps extends React.HTMLAttributes<HTMLDivElement> {
as?: typeof React.Component;
controlId?: string;
validationState?: 'success' | 'warning' | 'error' | null;
}
@@ -1,456 +0,0 @@
/*!
element styles of the minified bootstrap 3 css file
*/
@media print {
*,
*:before,
*:after {
background: transparent !important;
color: #000 !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
text-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]:after {
content: ' (' attr(href) ')';
}
abbr[title]:after {
content: ' (' attr(title) ')';
}
a[href^='#']:after,
a[href^='javascript:']:after {
content: '';
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
*:before,
*:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
font-size: 10px;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
body {
font-family: 'Lato', Helvetica, Arial, sans-serif;
font-size: 18px;
line-height: 1.42857143;
color: #333;
background-color: #fff;
}
input,
button,
select,
textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
a {
color: #006400;
text-decoration: none;
}
a:hover,
a:focus {
color: #001800;
text-decoration: underline;
}
figure {
margin: 0;
}
img {
vertical-align: middle;
}
hr {
margin-top: 25px;
margin-bottom: 25px;
border: 0;
border-top: 1px solid #eee;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.sr-only-focusable:active,
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
[role='button'] {
cursor: pointer;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: inherit;
font-weight: 400;
line-height: 1.1;
color: inherit;
}
h1 small,
h2 small,
h3 small,
h4 small,
h5 small,
h6 small {
font-weight: normal;
line-height: 1;
color: #777;
}
h1,
h2,
h3 {
margin-top: 25px;
margin-bottom: 12.5px;
}
h1 small,
h2 small,
h3 small {
font-size: 65%;
}
h4,
h5,
h6 {
margin-top: 12.5px;
margin-bottom: 12.5px;
}
h4 small,
h6 small {
font-size: 75%;
}
h1 {
font-size: 46px;
}
h2 {
font-size: 38px;
}
h3 {
font-size: 31px;
}
h4 {
font-size: 23px;
}
h5 {
font-size: 18px;
}
h6 {
font-size: 16px;
}
p {
margin: 0 0 12.5px;
}
small {
font-size: 88%;
}
mark {
background-color: #fcf8e3;
padding: 0.2em;
}
ul,
ol {
margin-top: 0;
margin-bottom: 12.5px;
}
ul ul,
ol ul,
ul ol,
ol ol {
margin-bottom: 0;
}
dl {
margin-top: 0;
margin-bottom: 25px;
}
dt,
dd {
line-height: 1.42857143;
}
dt {
font-weight: bold;
}
dd {
margin-left: 0;
}
abbr[title],
abbr[data-original-title] {
cursor: help;
border-bottom: 1px dotted #777;
}
blockquote {
padding: 12.5px 25px;
margin: 0 0 25px;
font-size: 22.5px;
border-left: 5px solid #eee;
}
blockquote p:last-child,
blockquote ul:last-child,
blockquote ol:last-child {
margin-bottom: 0;
}
blockquote footer,
blockquote small {
display: block;
font-size: 80%;
line-height: 1.42857143;
color: #777;
}
blockquote footer:before,
blockquote small:before {
content: '\2014 \00A0';
}
address {
margin-bottom: 25px;
font-style: normal;
line-height: 1.42857143;
}
code,
kbd,
pre,
samp {
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
}
code {
padding: 2px 4px;
font-size: 90%;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
}
kbd {
padding: 2px 4px;
font-size: 90%;
color: #fff;
background-color: #333;
border-radius: 3px;
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);
}
kbd kbd {
padding: 0;
font-size: 100%;
font-weight: bold;
-webkit-box-shadow: none;
box-shadow: none;
}
pre {
display: block;
padding: 12px;
margin: 0 0 12.5px;
font-size: 17px;
line-height: 1.42857143;
word-break: break-all;
word-wrap: break-word;
color: #333;
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
}
pre code {
padding: 0;
font-size: inherit;
color: inherit;
white-space: pre-wrap;
background-color: transparent;
border-radius: 0;
}
fieldset {
padding: 0;
margin: 0;
border: 0;
min-width: 0;
}
legend {
display: block;
width: 100%;
padding: 0;
margin-bottom: 25px;
font-size: 27px;
line-height: inherit;
color: #333;
border: 0;
border-bottom: 1px solid #e5e5e5;
}
label {
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
font-weight: bold;
}
input[type='search'] {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
input[type='radio'],
input[type='checkbox'] {
margin: 4px 0 0;
margin-top: 1px \9;
line-height: normal;
}
input[type='file'] {
display: block;
}
input[type='range'] {
display: block;
width: 100%;
}
select[multiple],
select[size] {
height: auto;
}
output {
display: block;
padding-top: 7px;
font-size: 18px;
line-height: 1.42857143;
color: #555;
}
input[type='search'] {
appearance: none;
}
input[type='radio'][disabled],
input[type='checkbox'][disabled],
fieldset[disabled] input[type='radio'],
fieldset[disabled] input[type='checkbox'] {
cursor: not-allowed;
}
/* Element styles rewrites from global.css that could be moved to presets*/
/* typography should be handled in tailwind config */
html {
height: 100%;
font-size: 18px;
-webkit-font-smoothing: antialiased;
}
body {
background-color: var(--secondary-background);
height: 100%;
font-family: var(--font-family-sans-serif);
color: var(--secondary-color);
}
header {
top: 0;
position: fixed;
width: 100%;
z-index: 200;
}
a {
color: inherit;
text-decoration: underline;
}
a:hover,
a:focus {
text-decoration: none;
}
input {
outline-color: transparent;
-webkit-box-shadow: none !important;
-moz-box-shadow: none !important;
box-shadow: none !important;
border-radius: 0;
}
textarea {
resize: vertical;
}
form {
margin-bottom: 5px;
}
code {
border-radius: 0;
overflow-wrap: anywhere;
}
hr {
border-top-width: 1px;
border-top-style: solid;
/* border-top-color: var(--quaternary-background); */
}
code[class*='language-'],
pre[class*='language-'] {
border-radius: 0;
}
pre {
color: inherit;
background-color: inherit;
border: none;
border-radius: 0;
}
blockquote footer,
blockquote small,
blockquote .small {
color: #858591;
}
/* WCAG fix */
.sr-only {
color: white;
background-color: black;
}
@@ -1,22 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { HelpBlock } from './help-block';
const story = {
title: 'Example/HelpBlock',
component: HelpBlock,
parameters: {
controls: {
include: ['className', 'children']
}
}
} satisfies Meta<typeof HelpBlock>;
type Story = StoryObj<typeof HelpBlock>;
export const Default: Story = {
args: {
children: 'This is a HelpBlock'
}
};
export default story;
@@ -1,14 +0,0 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { HelpBlock } from './help-block';
describe('Render the helpblock component', () => {
it('should render the helpblock component', () => {
render(<HelpBlock />);
expect(screen.getByTestId('help-block')).toBeInTheDocument();
expect(screen.getByTestId('help-block')).toHaveClass(
'block mt-1 mb-2 text-foreground-quaternary',
{ exact: true }
);
});
});
@@ -1,28 +0,0 @@
import React, { useContext } from 'react';
import { FormContext } from '../form-group/form-group';
const defaultClasses = 'block mt-1 mb-2';
const validationLabel = {
success: 'text-background-info',
warning: 'text-background-warning',
error: 'text-background-danger'
};
export const HelpBlock = React.forwardRef<
HTMLSpanElement,
React.ComponentProps<'span'>
>(({ className, children, ...props }, ref): JSX.Element => {
const { validationState } = useContext(FormContext);
const labelStyle = validationState
? validationLabel[validationState]
: 'text-foreground-quaternary';
const classes = [className, defaultClasses, labelStyle].join(' ');
return (
<span ref={ref} data-testid='help-block' className={classes} {...props}>
{children}
</span>
);
});
HelpBlock.displayName = 'HelpBlock';
@@ -1 +0,0 @@
export { HelpBlock } from './help-block';
@@ -1,37 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { Image } from './image';
// Additional type assertion necessary due to a bug of how Storybook handles the `decorators` property in meta
const story: Meta<typeof Image> = {
title: 'Example/Image',
component: Image,
argTypes: {
responsive: {
control: { type: 'boolean' }
}
},
decorators: [
Story => <div style={{ width: '100px', height: '100px' }}>{Story()}</div>
]
} satisfies Meta<typeof Image>;
type Story = StoryObj<typeof Image>;
export const Default: Story = {
args: {
alt: "camper's avatar",
src: 'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png',
responsive: false
}
};
export const Responsive: Story = {
args: {
alt: "camper's avatar",
src: 'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png',
responsive: true
}
};
export default story;
@@ -1,20 +0,0 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Image } from './image';
describe('Render the image', () => {
it('should render the image with the alt and src attributes', () => {
render(
<Image
alt="Quincy Larson's Signature"
src='https://cdn.freecodecamp.org/platform/english/images/quincy-larson-signature.svg'
/>
);
const image = screen.getByRole('img');
expect(image).toHaveAttribute(
'src',
'https://cdn.freecodecamp.org/platform/english/images/quincy-larson-signature.svg'
);
expect(image).toHaveAttribute('alt', "Quincy Larson's Signature");
});
});
-18
View File
@@ -1,18 +0,0 @@
import React from 'react';
import { ImageProps } from './image.types';
export const Image = React.forwardRef<HTMLImageElement, ImageProps>(
({ alt, src, responsive, className, ...props }, ref): JSX.Element => {
// If `responsive` is `true`:
// - Set `display` to `block`
// - Set a maximum width relative to the parent
// - Scale the height according to the width, otherwise the image is stretched
const classes = responsive
? [className, 'block max-w-full h-auto'].join(' ')
: className;
return <img ref={ref} alt={alt} src={src} className={classes} {...props} />;
}
);
Image.displayName = 'Image';
@@ -1,9 +0,0 @@
import React from 'react';
export interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
alt: string;
src: string;
/**
* Whether the image should be resized and contained within its parent.
*/
responsive?: boolean;
}
-1
View File
@@ -1 +0,0 @@
export { Image } from './image';
-19
View File
@@ -1,19 +0,0 @@
// Use this file as the entry point for component export
export { Alert, type AlertProps } from './alert';
export { Button, type ButtonProps } from './button';
export { CloseButton } from './close-button';
export { Image } from './image';
export { Table } from './table';
export { Panel } from './panel';
// export { ToggleButton } from './toggle-button';
export { Dropdown } from './drop-down';
export { MenuItem } from './drop-down/menu-item';
export { Container } from './container';
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
export { Col } from './col';
export { ControlLabel } from './control-label';
export { FormGroup, type FormGroupProps } from './form-group';
export { FormControl } from './form-control';
export { HelpBlock } from './help-block';
export { Row } from './row';
export { Modal, type ModalProps, type HeaderProps } from './modal';
-219
View File
@@ -1,219 +0,0 @@
import { Meta } from '@storybook/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='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>
-2
View File
@@ -1,2 +0,0 @@
export { Link } from './link';
export type { LinkProps } from './types';
@@ -1,26 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { Link } from '.';
const story = {
title: 'Example/Link',
component: Link
} satisfies Meta<typeof Link>;
type Story = StoryObj<typeof Link>;
export const Default: Story = {
args: {
children: 'Go to freeCodeCamp',
to: 'https://www.freecodecamp.org'
}
};
export const Block: Story = {
args: {
children: 'Go to freeCodeCamp',
to: 'https://www.freecodecamp.org',
block: true
}
};
export default story;
@@ -1,18 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Link } from '.';
describe('<Link />', () => {
it('should render correctly', () => {
const url = 'https://www.freecodecamp.org';
render(<Link to={url}>Go to freeCodeCamp</Link>);
expect(
screen.getByRole('link', { name: /Go to freeCodeCamp/i })
).toBeInTheDocument();
expect(
screen.getByRole('link', { name: /Go to freeCodeCamp/i })
).toHaveAttribute('href', url);
});
});
-36
View File
@@ -1,36 +0,0 @@
import React, { useMemo } from 'react';
import { LinkProps } from './types';
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
({ children, to, target, block }, ref) => {
const classes = useMemo(() => {
const cls = [
'text-foreground-primary',
'text-center',
'hover:no-underline',
'hover:bg-background-tertiary'
];
if (block) {
cls.push('block', 'w-full');
}
return cls.join(' ');
}, [block]);
return (
<a
className={classes}
href={to}
target={target}
ref={ref}
rel='noopener noreferrer'
>
{children}
</a>
);
}
);
Link.displayName = 'Link';
-7
View File
@@ -1,7 +0,0 @@
export interface LinkProps
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
children: React.ReactNode;
block?: boolean;
to?: string;
target?: React.HTMLAttributeAnchorTarget;
}
-2
View File
@@ -1,2 +0,0 @@
export { Modal } from './modal';
export type { ModalProps, HeaderProps } from './types';
@@ -1,178 +0,0 @@
import React, { useState } from 'react';
import { StoryObj, StoryFn, Meta } from '@storybook/react';
import { Button } from '../button';
import { Modal } from './modal';
import { type ModalProps, type HeaderProps, type BodyProps } from './types';
type StoryProps = ModalProps & HeaderProps & BodyProps;
type Story = StoryObj<StoryProps>;
const story = {
title: 'Example/Modal',
component: Modal,
args: {
size: 'medium',
variant: 'default'
},
argTypes: {
open: {
control: false
},
onClose: {
control: false
},
size: {
options: ['medium', 'large', 'xLarge']
},
variant: {
options: ['default', 'danger']
}
}
} satisfies Meta<typeof Modal>;
const Spacer = () => <div style={{ height: '5px', width: '100%' }} />;
const DefaultTemplate: StoryFn<StoryProps> = ({
showCloseButton,
alignment,
...modalProps
}) => {
const [open, setOpen] = useState(false);
const handleClose = () => setOpen(false);
return (
<div>
<Button onClick={() => setOpen(true)}>Open modal</Button>
<Modal {...modalProps} open={open} onClose={handleClose}>
<Modal.Header showCloseButton={showCloseButton}>
Lorem ipsum
</Modal.Header>
<Modal.Body alignment={alignment}>
<p>
Laboriosam autem non et nisi. Ut voluptatem sit beatae assumenda
amet aliquam corporis.
</p>
<p>
Dolores voluptas omnis et cupiditate ducimus delectus vel. Voluptas
atque cumque incidunt quia. A praesentium neque quis odit totam
praesentium illum est. Ut doloribus quisquam ut. Incidunt vel
suscipit accusamus consequuntur repellendus dolor sunt. Vel
accusamus nesciunt perspiciatis sunt est.
</p>
<p>
Tempore quis voluptas aut voluptatem praesentium nisi. Qui et quo ut
et vel dolores facilis dignissimos. Omnis facere quisquam recusandae
accusantium. Sit ut consectetur non id velit est odio. Laboriosam
soluta tenetur asperiores. Excepturi reprehenderit rerum sint
tempore molestiae vitae aliquid. Ea est sunt at atque ducimus
doloribus quas sit.
</p>
</Modal.Body>
<Modal.Footer>
<Button block size='large'>
Submit
</Button>
<Spacer />
<Button block size='large' onClick={handleClose}>
Cancel
</Button>
</Modal.Footer>
</Modal>
</div>
);
};
const DangerTemplate: StoryFn<ModalProps> = args => {
const [open, setOpen] = useState(false);
const handleClose = () => setOpen(false);
return (
<div>
<Button onClick={() => setOpen(true)}>Open modal</Button>
<Modal {...args} open={open} onClose={handleClose}>
<Modal.Header>Lorem ipsum</Modal.Header>
<Modal.Body>
<p>
Laboriosam autem non et nisi. Ut voluptatem sit beatae assumenda
amet aliquam corporis.
</p>
<p>
Dolores voluptas omnis et cupiditate ducimus delectus vel. Voluptas
atque cumque incidunt quia. A praesentium neque quis odit totam
praesentium illum est. Ut doloribus quisquam ut. Incidunt vel
suscipit accusamus consequuntur repellendus dolor sunt. Vel
accusamus nesciunt perspiciatis sunt est.
</p>
<p>
Tempore quis voluptas aut voluptatem praesentium nisi. Qui et quo ut
et vel dolores facilis dignissimos. Omnis facere quisquam recusandae
accusantium. Sit ut consectetur non id velit est odio. Laboriosam
soluta tenetur asperiores. Excepturi reprehenderit rerum sint
tempore molestiae vitae aliquid. Ea est sunt at atque ducimus
doloribus quas sit.
</p>
</Modal.Body>
<Modal.Footer>
<Button block size='large' onClick={handleClose}>
Cancel
</Button>
<Spacer />
<Button block variant='danger' size='large'>
Submit
</Button>
</Modal.Footer>
</Modal>
</div>
);
};
export const Default: Story = {
render: DefaultTemplate
};
export const Large: Story = {
render: DefaultTemplate,
args: {
size: 'large'
}
};
export const ExtraLarge: Story = {
render: DefaultTemplate,
args: {
size: 'xLarge'
}
};
export const Danger: Story = {
render: DangerTemplate,
args: {
variant: 'danger'
}
};
export const WithoutCloseButton: Story = {
render: DefaultTemplate,
args: {
showCloseButton: false
}
};
export const LeftAlignedBody: Story = {
render: DefaultTemplate,
args: {
alignment: 'left'
}
};
export const StartAlignedBody: Story = {
render: DefaultTemplate,
args: {
alignment: 'start'
}
};
export default story;
@@ -1,162 +0,0 @@
import React from 'react';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../button';
import { Modal } from './modal';
import { type ModalProps, type HeaderProps } from './types';
const originalWindow = window;
describe('<Modal />', () => {
beforeAll(() => {
// The Modal component uses `ResizeObserver` under the hood.
// However, this property is not available in JSDOM, so we need to manually add it to the window object.
// Ref: https://github.com/jsdom/jsdom/issues/3368
Object.defineProperty(window, 'ResizeObserver', {
writable: true,
value: jest.fn(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn()
}))
});
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
Object.defineProperty(globalThis, 'window', {
value: originalWindow
});
});
const setup = ({
showCloseButton,
open = false,
onClose = () => {},
...modalProps
}: Partial<ModalProps & HeaderProps>) => {
render(
<Modal {...modalProps} open={open} onClose={onClose}>
<Modal.Header showCloseButton={showCloseButton}>
Lorem ipsum
</Modal.Header>
<Modal.Body>
<p>Laboriosam autem non et nisi.</p>
</Modal.Body>
<Modal.Footer>
<Button block size='large'>
Submit
</Button>
<Button block size='large'>
Cancel
</Button>
</Modal.Footer>
</Modal>
);
};
it('should not appear if `open` is `false`', () => {
setup({ open: false });
expect(
screen.queryByRole('dialog', { name: 'Lorem ipsum' })
).not.toBeInTheDocument();
});
it('should appear and render the content properly if `open` is `true`', async () => {
setup({ open: true });
const dialog = await screen.findByRole('dialog', { name: 'Lorem ipsum' });
expect(dialog).toBeInTheDocument();
expect(
within(dialog).getByRole('heading', { level: 2 })
).toBeInTheDocument();
expect(
within(dialog).getByRole('button', { name: 'Close' })
).toBeInTheDocument();
expect(
within(dialog).getByText('Laboriosam autem non et nisi.')
).toBeInTheDocument();
expect(
within(dialog).getByRole('button', { name: 'Submit' })
).toBeInTheDocument();
expect(
within(dialog).getByRole('button', { name: 'Cancel' })
).toBeInTheDocument();
});
it('should hide the close button if `showCloseButton` is `false`', async () => {
setup({ open: true, showCloseButton: false });
const dialog = await screen.findByRole('dialog', { name: 'Lorem ipsum' });
expect(dialog).toBeInTheDocument();
expect(
within(dialog).getByRole('heading', { level: 2 })
).toBeInTheDocument();
expect(
within(dialog).queryByRole('button', { name: 'Close' })
).not.toBeInTheDocument();
expect(
within(dialog).getByText('Laboriosam autem non et nisi.')
).toBeInTheDocument();
expect(
within(dialog).getByRole('button', { name: 'Submit' })
).toBeInTheDocument();
expect(
within(dialog).getByRole('button', { name: 'Cancel' })
).toBeInTheDocument();
});
it('should automatically focus on the close button when open', async () => {
setup({ open: true });
const dialog = await screen.findByRole('dialog', { name: 'Lorem ipsum' });
expect(dialog).toBeInTheDocument();
const closeButton = within(dialog).getByRole('button', { name: 'Close' });
expect(closeButton).toHaveFocus();
});
it('should automatically focus on the first focusable element if `showCloseButton` is `false`', async () => {
setup({ open: true, showCloseButton: false });
const dialog = await screen.findByRole('dialog', { name: 'Lorem ipsum' });
expect(dialog).toBeInTheDocument();
const submitButton = within(dialog).getByRole('button', { name: 'Submit' });
expect(submitButton).toHaveFocus();
});
it('should trigger the `onClose` prop on close button click', async () => {
const onClose = jest.fn();
setup({ open: true, onClose });
const dialog = await screen.findByRole('dialog', { name: 'Lorem ipsum' });
expect(dialog).toBeInTheDocument();
const closeButton = within(dialog).getByRole('button', { name: 'Close' });
await userEvent.click(closeButton);
expect(onClose).toHaveBeenCalledTimes(1);
});
it('should trigger the `onClose` prop on Escape key press', async () => {
const onClose = jest.fn();
setup({ open: true, onClose });
const dialog = await screen.findByRole('dialog', { name: 'Lorem ipsum' });
expect(dialog).toBeInTheDocument();
await userEvent.keyboard('{Escape}');
expect(onClose).toHaveBeenCalledTimes(1);
});
});
-156
View File
@@ -1,156 +0,0 @@
import React, { createContext, useContext, Fragment } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import { CloseButton } from '../close-button';
import type { ModalProps, HeaderProps, BodyProps, FooterProps } from './types';
// There is a close button on the right side of the modal title.
// Some extra padding needs to be added to the left of the title text
// so that the title is properly centered.
// The value of the left padding is the width of the close button.
const TITLE_LEFT_PADDING = 24;
const PANEL_DEFAULT_CLASSES =
'flex flex-col border-solid border-1 border-foreground-secondary bg-background-secondary';
const HEADER_DEFAULT_CLASSES = 'p-[15px]';
const ModalContext = createContext<Pick<ModalProps, 'onClose' | 'variant'>>({
onClose: () => {},
variant: 'default'
});
const Header = ({
children,
showCloseButton = true,
closeButtonClassNames,
borderless
}: HeaderProps) => {
const { onClose, variant } = useContext(ModalContext);
let classes = HEADER_DEFAULT_CLASSES;
if (variant === 'danger') {
classes = classes.concat(' ', 'bg-foreground-danger');
}
if (!borderless) {
classes = classes.concat(
' ',
'border-b-1 border-solid border-foreground-secondary'
);
}
if (showCloseButton) {
classes = classes.concat(' ', 'flex items-center justify-between');
return (
<div className={classes}>
<Dialog.Title
className={`m-0 pl-[${TITLE_LEFT_PADDING}px] flex-1 text-md text-center`}
>
{children}
</Dialog.Title>
<CloseButton onClick={onClose} className={closeButtonClassNames} />
</div>
);
}
return (
<div className={classes}>
<Dialog.Title className='m-0 text-md text-center'>
{children}
</Dialog.Title>
</div>
);
};
const Body = ({
children,
alignment = 'center',
borderless,
className
}: BodyProps) => {
const borderClasses = borderless
? ''
: 'border-b-1 border-solid border-foreground-secondary';
return (
<div className={`p-[15px] text-${alignment} ${borderClasses} ${className}`}>
{children}
</div>
);
};
const Footer = ({ children, alignment = 'center' }: FooterProps) => {
if (alignment === 'end') {
return <div className='p-[15px] flex justify-end'>{children}</div>;
}
return (
<div className={`p-[15px] flex flex-col justify-center`}>{children}</div>
);
};
const Modal = ({
children,
open,
onClose,
onKeyDown,
size = 'medium',
variant = 'default'
}: ModalProps) => {
let panelClasses = PANEL_DEFAULT_CLASSES;
if (size === 'medium') {
panelClasses = panelClasses.concat(' ', 'w-[600px]');
} else if (size === 'large') {
panelClasses = panelClasses.concat(' ', 'w-[900px]');
} else if (size === 'xLarge') {
panelClasses = panelClasses.concat(' ', 'w-[95vw] md:w-[90vw]');
}
if (variant === 'default') {
panelClasses = panelClasses.concat(' ', 'text-foreground-secondary');
} else if (variant === 'danger') {
panelClasses = panelClasses.concat(' ', 'text-background-danger');
}
return (
<ModalContext.Provider value={{ onClose, variant }}>
<Transition.Root show={open} as={Fragment}>
<Dialog
onClose={onClose}
className='relative z-1050 w-screen h-screen'
onKeyDown={onKeyDown}
>
{/* The backdrop, rendered as a fixed sibling to the panel container */}
<div aria-hidden className='fixed inset-0 bg-gray-900 opacity-50' />
{/* Full-screen container of the panel */}
<div
className={`fixed inset-0 flex items-start justify-center p-[10px] md:pt-[30px] md:pb-[30px] overflow-scroll`}
>
<Transition.Child
as={Fragment}
enter='transition-all duration-300 ease-out'
enterFrom='opacity-0 -translate-y-1/4'
enterTo='opacity-100 translate-y-0'
leave='transition-all duration-300 ease-out'
leaveFrom='opacity-100 translate-y-0'
leaveTo='opacity-0 -translate-y-1/4'
>
<Dialog.Panel className={panelClasses}>{children}</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
</ModalContext.Provider>
);
};
Modal.Header = Header;
Modal.Body = Body;
Modal.Footer = Footer;
export { Modal };
-49
View File
@@ -1,49 +0,0 @@
import { type ReactNode } from 'react';
export interface ModalProps {
children: ReactNode;
open: boolean;
onClose: () => void;
onKeyDown?: React.KeyboardEventHandler;
size?: 'medium' | 'large' | 'xLarge';
variant?: 'default' | 'danger';
}
export interface HeaderProps {
children: ReactNode;
showCloseButton?: boolean;
/**
* /learn uses the `button` selector to override Bootstrap button styles,
* and the selector also affects `ui-components`.
* We need to forward the class specific to the close button from /learn
* in order for the button to display properly.
* Ref: https://github.com/freeCodeCamp/freeCodeCamp/blob/c1137b0900ad098fee2fca430d11f69dfc909d3b/client/src/components/layouts/global.css#L179-L192
* NOTE: This prop is temporary and should be removed once we remove all CSS overrides from /learn.
* Tracking issue: https://github.com/freeCodeCamp/freeCodeCamp/issues/52715
*/
closeButtonClassNames?: string;
borderless?: boolean;
}
export interface BodyProps {
children: ReactNode;
/**
* Use `start` for better right-to-left support.
* Use `left` if the modal body contains code blocks.
*/
alignment?: 'center' | 'left' | 'start';
borderless?: boolean;
/**
* This is an escape hatch for cases where the component doesn't meet your design requirements.
* Use this option sparingly.
*/
className?: string;
}
export interface FooterProps {
children: ReactNode;
alignment?: 'center' | 'end';
}

Some files were not shown because too many files have changed in this diff Show More