From f7d315900ab3d4b417af6bbd5a592b924a61d20f Mon Sep 17 00:00:00 2001 From: theojungle Date: Wed, 18 Dec 2024 17:44:55 +0100 Subject: [PATCH 1/9] chore: mono package is back! --- .circleci/config.yml | 3 + lib/.eslintignore | 1 + lib/.eslintrc.ts | 3 + lib/package.json | 74 ++ lib/prettier.config.js | 1 + lib/scripts/generate-types-doc.ts | 131 +++ lib/scripts/generate-website-examples.ts | 56 ++ lib/scripts/get-components-entry.ts | 37 + .../components/Box/docs/examples/example.tsx | 43 + .../components/Box/docs/examples/overview.tsx | 19 + lib/src/components/Box/docs/index.mdx | 10 + lib/src/components/Box/index.tsx | 10 + lib/src/components/Box/tests/index.test.tsx | 13 + lib/src/components/System/index.tsx | 160 ++++ lib/src/components/WuiProvider/font.ts | 78 ++ lib/src/components/WuiProvider/fonts.test.ts | 27 + .../WuiProvider/hide-focus-rings-root.tsx | 52 ++ lib/src/components/WuiProvider/index.tsx | 40 + lib/src/components/WuiProvider/styles.ts | 404 +++++++++ lib/src/components/index.tsx | 3 + lib/src/index.tsx | 2 + lib/src/theme/borders.ts | 11 + lib/src/theme/colors.ts | 133 +++ lib/src/theme/dark.ts | 31 + lib/src/theme/focus.ts | 17 + lib/src/theme/fonts.ts | 64 ++ lib/src/theme/index.ts | 193 +++++ lib/src/theme/radii.ts | 24 + lib/src/theme/screens.ts | 23 + lib/src/theme/selection.ts | 12 + lib/src/theme/shadows.ts | 9 + lib/src/theme/space.ts | 35 + lib/src/theme/transitions.ts | 25 + lib/src/theme/typography.ts | 301 +++++++ lib/src/theme/underline.ts | 32 + lib/stylelint.config.js | 3 + lib/tests/index.tsx | 38 + lib/tsconfig.json | 12 + lib/utils/mergeDeepRight.ts | 19 + lib/vite.config.mjs | 47 ++ migrated_packages.ts | 1 + package.json | 6 +- tsconfig.json | 9 +- .../app/components/[id]/[subPage]/code.tsx | 14 +- .../app/components/[id]/[subPage]/other.tsx | 6 +- .../app/components/[id]/[subPage]/props.tsx | 4 +- website/app/components/[id]/layout.tsx | 47 +- website/app/components/[id]/page.tsx | 12 +- website/app/components/page.tsx | 5 +- .../app/foundations/[id]/[subPage]/page.tsx | 6 +- website/app/foundations/[id]/page.tsx | 4 +- .../components/Installation/index.tsx | 18 +- website/build-app/components/Mdx/Div.tsx | 33 +- .../build-app/components/Mdx/Playground.tsx | 8 +- .../components/ThemeProvider/index.tsx | 18 +- website/build-app/examples.js | 4 +- .../build-app/utils/components-properties.ts | 7 +- website/build-app/utils/page-content.ts | 15 +- website/build-app/utils/pages-components.ts | 13 +- yarn.lock | 799 +++++++++++++++++- 60 files changed, 3112 insertions(+), 113 deletions(-) create mode 100644 lib/.eslintignore create mode 100644 lib/.eslintrc.ts create mode 100644 lib/package.json create mode 100644 lib/prettier.config.js create mode 100644 lib/scripts/generate-types-doc.ts create mode 100644 lib/scripts/generate-website-examples.ts create mode 100644 lib/scripts/get-components-entry.ts create mode 100644 lib/src/components/Box/docs/examples/example.tsx create mode 100644 lib/src/components/Box/docs/examples/overview.tsx create mode 100644 lib/src/components/Box/docs/index.mdx create mode 100644 lib/src/components/Box/index.tsx create mode 100644 lib/src/components/Box/tests/index.test.tsx create mode 100644 lib/src/components/System/index.tsx create mode 100644 lib/src/components/WuiProvider/font.ts create mode 100644 lib/src/components/WuiProvider/fonts.test.ts create mode 100644 lib/src/components/WuiProvider/hide-focus-rings-root.tsx create mode 100644 lib/src/components/WuiProvider/index.tsx create mode 100644 lib/src/components/WuiProvider/styles.ts create mode 100644 lib/src/components/index.tsx create mode 100644 lib/src/index.tsx create mode 100644 lib/src/theme/borders.ts create mode 100644 lib/src/theme/colors.ts create mode 100644 lib/src/theme/dark.ts create mode 100644 lib/src/theme/focus.ts create mode 100644 lib/src/theme/fonts.ts create mode 100644 lib/src/theme/index.ts create mode 100644 lib/src/theme/radii.ts create mode 100644 lib/src/theme/screens.ts create mode 100644 lib/src/theme/selection.ts create mode 100644 lib/src/theme/shadows.ts create mode 100644 lib/src/theme/space.ts create mode 100644 lib/src/theme/transitions.ts create mode 100644 lib/src/theme/typography.ts create mode 100644 lib/src/theme/underline.ts create mode 100644 lib/stylelint.config.js create mode 100644 lib/tests/index.tsx create mode 100644 lib/tsconfig.json create mode 100644 lib/utils/mergeDeepRight.ts create mode 100644 lib/vite.config.mjs create mode 100644 migrated_packages.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 4934881984..e687985aa2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,6 +48,7 @@ jobs: paths: - ~/.cache/yarn - ~/welcome-ui/node_modules + - ~/welcome-ui/lib/node_modules - ~/welcome-ui/website/node_modules vulnerabilities_yarn: @@ -71,11 +72,13 @@ jobs: - *checkout - *restore_node_modules - run: yarn icons:build + - run: yarn build:monorepo - run: yarn build - persist_to_workspace: root: ~/welcome-ui paths: - packages/**/dist + - lib/dist - packages/Themes/**/dist - icons/dist - packages/IconFont/fonts diff --git a/lib/.eslintignore b/lib/.eslintignore new file mode 100644 index 0000000000..1521c8b765 --- /dev/null +++ b/lib/.eslintignore @@ -0,0 +1 @@ +dist diff --git a/lib/.eslintrc.ts b/lib/.eslintrc.ts new file mode 100644 index 0000000000..14feefebfb --- /dev/null +++ b/lib/.eslintrc.ts @@ -0,0 +1,3 @@ +module.exports = { + extends: './../node_modules/wttj-config/lib/eslint/eslintrc-typescript', +} diff --git a/lib/package.json b/lib/package.json new file mode 100644 index 0000000000..61cd0b5280 --- /dev/null +++ b/lib/package.json @@ -0,0 +1,74 @@ +{ + "name": "welcome-ui", + "version": "1.0.0-beta.2", + "description": "Customizable design system with react • styled-components • styled-system and ariakit.", + "files": [ + "dist" + ], + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./*": { + "import": "./dist/components/*.mjs", + "require": "./dist/components/*.js" + } + }, + "scripts": { + "start": "vite build --watch", + "build": "vite build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/WTTJ/welcome-ui.git" + }, + "keywords": [ + "design-system", + "react", + "ariakit", + "styled-components", + "styled-system", + "ui-library", + "ui", + "ux", + "welcome", + "WTTJ" + ], + "author": "WTTJ ", + "license": "MIT", + "bugs": { + "url": "https://github.com/WTTJ/welcome-ui/issues" + }, + "dependencies": { + "@ariakit/react": "0.4.15" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", + "@types/react": "^18.3.12", + "@xstyled/styled-components": "^3.7.3", + "react": "^18.0.0", + "react-docgen-typescript": "^2.2.2", + "rollup-preserve-directives": "^1.1.3", + "styled-components": "^5.3.9", + "typescript": "^5.6.3", + "vite": "^5.4.10", + "vite-plugin-dts": "^4.3.0", + "wttj-config": "^3.0.1" + }, + "peerDependencies": { + "@xstyled/styled-components": "^3.7.3", + "react": "^18.0.0", + "styled-components": "^5.3.9" + }, + "resolutions": { + "loader-utils": "2.0.3" + }, + "homepage": "https://welcome-ui.com" +} diff --git a/lib/prettier.config.js b/lib/prettier.config.js new file mode 100644 index 0000000000..f43980fa0b --- /dev/null +++ b/lib/prettier.config.js @@ -0,0 +1 @@ +module.exports = require('wttj-config/lib/prettier') diff --git a/lib/scripts/generate-types-doc.ts b/lib/scripts/generate-types-doc.ts new file mode 100644 index 0000000000..b9c760bfa1 --- /dev/null +++ b/lib/scripts/generate-types-doc.ts @@ -0,0 +1,131 @@ +import { join, resolve } from 'path' +import fs, { existsSync } from 'fs' + +import docgen from 'react-docgen-typescript' + +const tsConfigPath = join(process.cwd(), 'tsconfig.json') + +const shouldDisplayPropsFiles = [ + 'packages/Utils/dist/types/field-styles.d.ts', + 'packages/Button/dist/types/index.d.ts', + 'packages/InputText/dist/types/index.d.ts', + 'welcome-ui/node_modules/ariakit/ts/Tab/TabStore.d.ts', +] + +// Get only ComponentOptions declarations for prevent all WuiProps +const propFilter = prop => { + if (prop.declarations?.length > 0) { + const isOptionDeclaration = prop.declarations.find(declaration => { + if (declaration.name.includes('Options')) return true + + return shouldDisplayPropsFiles.includes(declaration.fileName) + }) + + return Boolean(isOptionDeclaration) + } + + return true +} + +const { parse } = docgen.withCustomConfig(tsConfigPath, { + propFilter, + shouldRemoveUndefinedFromOptional: true, + shouldExtractValuesFromUnion: true, +}) + +const isComponentFile = file => { + if (file === 'index.tsx') { + return true + } + + const [name, extension] = file.split('.') + const firstLetter = name[0] + + // Components start with capital letter e.g. Title.js + if (extension === 'tsx' && firstLetter === firstLetter.toUpperCase()) { + return true + } + + return false +} + +// Get all files in a component folder +const getComponentFiles = async (folder: string) => { + const componentFiles = await fs.readdirSync(folder) + + return componentFiles.filter(isComponentFile) +} + +// Get definitions from file +const getFileDefinitions = absolutePath => { + const definitions = parse(absolutePath) + + return definitions +} + +// Write properties.json file +const writePropsFile = async (file, content) => { + const withDocsPath = existsSync(join(file, 'docs')) + + if (withDocsPath) { + const destPath = join(file, 'docs', 'properties.json') + + await fs.writeFileSync(destPath, JSON.stringify(content, null, 2)) + } + + return +} + +export async function generateTypesDoc() { + const parentDirectory = resolve(__dirname, '../') + const componentsDir = resolve(parentDirectory, 'src/components') + + // Read all directories in the components folder with a docs folder + const componentDirs = fs + .readdirSync(componentsDir, { withFileTypes: true }) + .filter(dirent => { + if (!dirent.isDirectory()) return false + // Check if the directory has a docs folder + try { + fs.accessSync(join(componentsDir, dirent.name, 'docs')) + return true + } catch { + return false + } + }) + .map(dirent => dirent.name) + + // Get all files in each component folder + componentDirs.map(async dirent => { + const files = await getComponentFiles(resolve(parentDirectory, 'src/components', dirent)) + + // Get definitions from each file + files.forEach(async file => { + const absolutePath = join(process.cwd(), 'src', 'components', dirent) + const definitions = getFileDefinitions(`${absolutePath}/${file}`) + const componentProps = {} + + definitions.forEach(definition => { + const { displayName, props, tags } = definition + const name = tags?.name || displayName + + if (props) { + componentProps[name] = { + tag: tags?.tag, + props: Object.keys(props) + .sort() + .reduce((obj, key) => { + obj[key] = props[key] + return obj + }, {}), + } + } + }) + + // Write properties.json file check before if has no props + if (componentProps?.[0]?.props?.length > 0) { + await writePropsFile(absolutePath, componentProps) + } + }) + }) +} diff --git a/lib/scripts/generate-website-examples.ts b/lib/scripts/generate-website-examples.ts new file mode 100644 index 0000000000..692759ca32 --- /dev/null +++ b/lib/scripts/generate-website-examples.ts @@ -0,0 +1,56 @@ +import { existsSync, readdirSync, writeFileSync } from 'fs' +import { join, resolve } from 'path' + +import { MIGRATED_PACKAGES } from '../../migrated_packages' + +function generateWebsiteExamplesPages() { + const parentDirectory = resolve(__dirname, '../../') + const packagesDirectory = join(parentDirectory, 'packages') + const packagesDirectoryExist = existsSync(packagesDirectory) + + const examples = [] as string[] + + if (!packagesDirectoryExist) return + + const folderList = readdirSync(packagesDirectory) + + for (const folder of folderList) { + const isNewPackage = MIGRATED_PACKAGES.includes(folder) + + const subFolder = isNewPackage + ? join(parentDirectory, 'lib', 'src', 'components', folder, 'docs', 'examples') + : join(packagesDirectory, folder, 'docs', 'examples') + const subFolderExist = existsSync(subFolder) + + if (!subFolderExist) continue + + const fileList = readdirSync(subFolder) + + for (const file of fileList) { + examples.push( + isNewPackage + ? `${subFolder}/${file}`.split('components')[1] + : `${subFolder}/${file}`.split('packages')[1] + ) + } + } + + const fileContent = `/* eslint-disable */\n/** WARNING\nThis file is auto-generate with yarn watch command, do not change it directly!\n**/\n\nimport dynamic from "next/dynamic";\n\nexport default {\n${examples + .map( + path => + ` "${path}": dynamic(() => ${MIGRATED_PACKAGES.some(pkg => path.startsWith(`/${pkg}`)) ? 'import("../../lib/src/components' : 'import("../../packages'}${path}").then(mod => mod), { ssr: false })` + ) + .join(',\n')}\n};\n` + + writeFileSync(join(parentDirectory, 'website', 'build-app', 'examples.js'), fileContent) +} + +export const generateWebsiteExamplesPlugin = () => { + return { + name: 'website-examples', + // generate website examples for NextJS static pages + writeBundle() { + generateWebsiteExamplesPages() + }, + } +} diff --git a/lib/scripts/get-components-entry.ts b/lib/scripts/get-components-entry.ts new file mode 100644 index 0000000000..227bc36286 --- /dev/null +++ b/lib/scripts/get-components-entry.ts @@ -0,0 +1,37 @@ +import { resolve } from 'path' +import fs from 'fs' + +export const getComponentsEntries = () => { + const parentDirectory = resolve(__dirname, '../') + const componentsDir = resolve(parentDirectory, 'src/components') + + // Read all directories in the components folder + const componentDirs = fs + .readdirSync(componentsDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name) + + // Create entry object dynamically + const entries = componentDirs.reduce( + (acc, componentName) => { + const entryPath = resolve(componentsDir, componentName, 'index.tsx') + const kababCaseName = componentName + .replace(/([a-z])([A-Z])/g, '$1-$2') + .replace(/[\s_]+/g, '-') + .toLowerCase() + + // Only add to entries if the index.tsx file exists + if (fs.existsSync(entryPath)) { + acc[`${kababCaseName}`] = entryPath + } + + return acc + }, + { + // Always include the main index entry + index: resolve(parentDirectory, 'src/index.tsx'), + } + ) + + return entries +} diff --git a/lib/src/components/Box/docs/examples/example.tsx b/lib/src/components/Box/docs/examples/example.tsx new file mode 100644 index 0000000000..17e8d18be8 --- /dev/null +++ b/lib/src/components/Box/docs/examples/example.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' +import { Box } from 'welcome-ui' +import { Badge } from '@welcome-ui/badge' +import { StarIcon } from '@welcome-ui/icons' +import { Text } from '@welcome-ui/text' + +const Example = () => { + return ( + + + + + + Superhost + + + 4.8/5 + + + + Jungle House in the middle of nowhere + + + 890€/week{' • '}2 beds + + + + + ) +} + +export default Example diff --git a/lib/src/components/Box/docs/examples/overview.tsx b/lib/src/components/Box/docs/examples/overview.tsx new file mode 100644 index 0000000000..6a08c2a39b --- /dev/null +++ b/lib/src/components/Box/docs/examples/overview.tsx @@ -0,0 +1,19 @@ +import * as React from 'react' +import { Box } from 'welcome-ui' + +const Example = () => { + return ( + + This is a Box with style form theme + + ) +} + +export default Example diff --git a/lib/src/components/Box/docs/index.mdx b/lib/src/components/Box/docs/index.mdx new file mode 100644 index 0000000000..a7fb25dc55 --- /dev/null +++ b/lib/src/components/Box/docs/index.mdx @@ -0,0 +1,10 @@ +--- +category: layout +description: The Box component is the most basic component of Welcome UI — all other components make use of it for styling. By default, it’s a div element. It provides a flexible and consistent foundation for building and styling other UI elements, ensuring uniformity and ease of customization across the application. +packageName: box +title: Box +--- + +### Customize + +
diff --git a/lib/src/components/Box/index.tsx b/lib/src/components/Box/index.tsx new file mode 100644 index 0000000000..013ce1256c --- /dev/null +++ b/lib/src/components/Box/index.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { x } from '@xstyled/styled-components' + +import { CreateWuiProps, forwardRef } from '../System' + +export type BoxProps = Omit, 'dataTestId'> + +export const Box = forwardRef<'div', BoxProps>((props, ref) => { + return +}) diff --git a/lib/src/components/Box/tests/index.test.tsx b/lib/src/components/Box/tests/index.test.tsx new file mode 100644 index 0000000000..0e7567a86a --- /dev/null +++ b/lib/src/components/Box/tests/index.test.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +import { Box } from '../' + +import { render } from './../../../../../utils/tests' + +describe('', () => { + it('should render correctly', () => { + const { container } = render(children) + + expect(container).toHaveTextContent('children') + }) +}) diff --git a/lib/src/components/System/index.tsx b/lib/src/components/System/index.tsx new file mode 100644 index 0000000000..45667b282d --- /dev/null +++ b/lib/src/components/System/index.tsx @@ -0,0 +1,160 @@ +import React from 'react' +import { + compose, + getPx, + getTransition, + getZIndex, + Props, + style, + SystemProps, +} from '@xstyled/styled-components' +import * as S from '@xstyled/styled-components' +import { StyledConfig } from 'styled-components' + +// Those are styles that were in v1 but not in v2 +const oldProps = compose( + style({ prop: 'opacity' }), + style({ prop: 'overflow' }), + style({ prop: 'transition', themeGet: getTransition }), + style({ prop: 'position' }), + style({ prop: 'zIndex', themeGet: getZIndex }), + style({ prop: 'top', themeGet: getPx }), + style({ prop: 'right', themeGet: getPx }), + style({ prop: 'bottom', themeGet: getPx }), + style({ prop: 'left', themeGet: getPx }) +) + +const SYSTEM_PROPS = Object.freeze([ + S.backgrounds, + S.borders, + S.boxShadow, + S.color, + S.display, + S.flexboxes, + S.grids, + S.height, + S.maxHeight, + S.maxWidth, + S.minHeight, + S.minWidth, + S.space, + S.typography, + S.verticalAlign, + S.width, + oldProps, +]) + +const WRAPPER_PROPS = Object.freeze([ + S.margin, + S.marginBottom, + S.marginLeft, + S.marginRight, + S.marginTop, + S.mx, + S.my, + S.width, + oldProps, +]) + +/** + * @deprecated use system from @xstyled/syled-components instead + */ +export const system = compose(...SYSTEM_PROPS) +/** + * @deprecated use system from @xstyled/syled-components instead + */ +export const wrapperSystem = compose(...WRAPPER_PROPS) +const componentProps = system.meta.props + .filter(prop => !wrapperSystem.meta.props.includes(prop)) + .map(prop => { + if (prop === 'w') return S['width'] + if (prop === 'h') return S['height'] + return (S as Props)[prop] + }) + .filter(Boolean) +/** + * @deprecated use system from @xstyled/syled-components instead + */ +export const componentSystem = compose(...componentProps) + +export const filterSystemProps = (prop: string): boolean => !system.meta.props.includes(prop) +export const shouldForwardProp: StyledConfig['shouldForwardProp'] = (prop, defaultValidatorFn) => + defaultValidatorFn(prop) + +export type WuiOldProps = S.OpacityProps & + S.OverflowProps & + S.TransitionProps & + S.ZIndexProps & + S.TopProps & + S.RightProps & + S.BottomProps & + S.LeftProps + +export type WuiSystemProps = S.BackgroundsProps & + S.BorderProps & + S.BoxShadowProps & + S.ColorProps & + S.DisplayProps & + S.FlexboxesProps & + S.GridsProps & + S.HeightProps & + S.MaxHeightProps & + S.MaxWidthProps & + S.MinHeightProps & + S.MinWidthProps & + S.SpaceProps & + S.TypographyProps & + S.VerticalAlignProps & + S.WidthProps & + WuiOldProps + +export type WuiWrapperSystemProps = S.MarginProps & + S.MarginBottomProps & + S.MarginLeftProps & + S.MarginRightProps & + S.MarginTopProps & + S.MarginXProps & + S.MarginYProps & + S.WidthProps & + WuiOldProps + +export interface WuiTestProps { + dataTestId?: string +} + +export type WuiProps = SystemProps + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type As = React.ElementType + +export type RightJoinProps = Omit & + OverrideProps + +export type MergeProps = RightJoinProps & + RightJoinProps + +// eslint-disable-next-line @typescript-eslint/ban-types +export type CreateWuiProps = MergeProps< + Omit, keyof WuiProps>, + Props, + WuiProps & WuiTestProps & { as?: As } +> + +// eslint-disable-next-line @typescript-eslint/ban-types +export type CreateWuiComponent = { + ( + props: CreateWuiProps & { as: AsComponent } + ): JSX.Element + (props: CreateWuiProps): JSX.Element + displayName?: string +} + +// eslint-disable-next-line @typescript-eslint/ban-types +export const forwardRef = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + component: React.ForwardRefRenderFunction +): CreateWuiComponent => { + return React.forwardRef(component) as unknown as CreateWuiComponent +} + +export type ExtraSize = number | string diff --git a/lib/src/components/WuiProvider/font.ts b/lib/src/components/WuiProvider/font.ts new file mode 100644 index 0000000000..b48f3c7b9c --- /dev/null +++ b/lib/src/components/WuiProvider/font.ts @@ -0,0 +1,78 @@ +import { css } from '@xstyled/styled-components' + +import { ThemeValues } from '../../theme' + +type FontVariation = { + display?: FontDisplay + extensions?: string[] + isVariable?: boolean + style?: string + unicodeRange?: string + url: string + weight?: string +} + +type Font = { + name: string + variation: FontVariation +} + +export function getSource( + url: FontVariation['url'], + extensions: FontVariation['extensions'], + isVariable: FontVariation['isVariable'] +) { + /** variable icon font */ + if (isVariable) { + return extensions + .map((extension: string) => `url('${url}.${extension}') format('${extension}-variations')`) + .join(', ') + } + + return extensions + .map((extension: string) => `url('${url}.${extension}') format('${extension}')`) + .join(', ') +} + +function getFont({ + name, + variation: { + display = 'swap', + extensions = ['woff2', 'woff'], + isVariable, + style, + unicodeRange, + url, + weight, + }, +}: Font) { + return css` + @font-face { + font-family: ${name}; + src: ${getSource(url, extensions, isVariable)}; + font-display: ${display}; + ${weight && + css` + font-weight: ${weight}; + `} + ${style && + css` + font-style: ${style}; + `} + ${unicodeRange && + css` + unicode-range: ${unicodeRange}; + `} + } + ` +} + +export const fonts = + () => + ({ theme }: { theme: ThemeValues }): ReturnType => { + if (!theme || !theme.fontFaces) return null + + return Object.entries(theme.fontFaces).map(([name, variations]) => + variations.map(variation => getFont({ name, variation })) + ) + } diff --git a/lib/src/components/WuiProvider/fonts.test.ts b/lib/src/components/WuiProvider/fonts.test.ts new file mode 100644 index 0000000000..5724dcc6ba --- /dev/null +++ b/lib/src/components/WuiProvider/fonts.test.ts @@ -0,0 +1,27 @@ +import { getSource } from './font' + +describe('getSource', () => { + it('should return correct font-face rules if not variable', () => { + const directive = getSource( + 'https://cdn.welcometothejungle.com/fonts/welcome-font-regular', + ['woff', 'woff2'], + false + ) + + expect(directive).toBe( + "url('https://cdn.welcometothejungle.com/fonts/welcome-font-regular.woff') format('woff'), url('https://cdn.welcometothejungle.com/fonts/welcome-font-regular.woff2') format('woff2')" + ) + }) + + it('should return correct font-face rules if variable', () => { + const directive = getSource( + 'https://cdn.welcometothejungle.com/fonts/welcome-font-regular', + ['woff', 'woff2'], + true + ) + + expect(directive).toBe( + "url('https://cdn.welcometothejungle.com/fonts/welcome-font-regular.woff') format('woff-variations'), url('https://cdn.welcometothejungle.com/fonts/welcome-font-regular.woff2') format('woff2-variations')" + ) + }) +}) diff --git a/lib/src/components/WuiProvider/hide-focus-rings-root.tsx b/lib/src/components/WuiProvider/hide-focus-rings-root.tsx new file mode 100644 index 0000000000..388c5d33c9 --- /dev/null +++ b/lib/src/components/WuiProvider/hide-focus-rings-root.tsx @@ -0,0 +1,52 @@ +import React, { useEffect, useState } from 'react' +import { createGlobalStyle, css } from '@xstyled/styled-components' + +export const hideFocusRingsDataAttribute = 'data-wui-hidefocusrings' + +// Based on braid's solution: +// https://github.com/seek-oss/braid-design-system/blob/9ea04cade0303309e956d943e88a9e613a19c333/lib/components/private/hideFocusRings/HideFocusRingsRoot.tsx + +const HideFocusRingGlobalStyles = createGlobalStyle( + () => css` + [${hideFocusRingsDataAttribute}] *:focus { + outline: none; + } + ` +) + +interface HideFocusRingsRootProps { + children?: React.ReactNode + reactRootId: string +} + +export const HideFocusRingsRoot: React.FC = ({ + children, + reactRootId, +}) => { + const [hideFocusRings, setHideFocusRings] = useState(false) + + useEffect(() => { + const eventName = hideFocusRings ? 'keydown' : 'mousemove' + const toggleFocusRings = () => setHideFocusRings(x => !x) + + window.addEventListener(eventName, toggleFocusRings) + + const rootElement = document.getElementById(reactRootId) + if (rootElement) { + hideFocusRings + ? rootElement.setAttribute(hideFocusRingsDataAttribute, 'true') + : rootElement.removeAttribute(hideFocusRingsDataAttribute) + } + + return () => { + window.removeEventListener(eventName, toggleFocusRings) + } + }, [hideFocusRings, reactRootId]) + + return ( + <> + + {children} + + ) +} diff --git a/lib/src/components/WuiProvider/index.tsx b/lib/src/components/WuiProvider/index.tsx new file mode 100644 index 0000000000..2c3bae1e59 --- /dev/null +++ b/lib/src/components/WuiProvider/index.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { ThemeProvider } from '@xstyled/styled-components' + +import { ThemeValues } from '../../theme' + +import * as S from './styles' +import { HideFocusRingsRoot } from './hide-focus-rings-root' + +export interface WuiProviderProps { + children?: React.ReactNode + hasGlobalStyle?: boolean + reactRootId?: string + shouldHideFocusRingOnClick?: boolean + theme: ThemeValues + useReset?: boolean +} + +export const WuiProvider: React.FC = ({ + children, + hasGlobalStyle = true, + reactRootId = 'root', + shouldHideFocusRingOnClick = true, + theme, + useReset, +}) => { + return ( + + <> + {hasGlobalStyle && } + {shouldHideFocusRingOnClick ? ( + {children} + ) : ( + children + )} + + + ) +} + +WuiProvider.displayName = 'WuiProvider' diff --git a/lib/src/components/WuiProvider/styles.ts b/lib/src/components/WuiProvider/styles.ts new file mode 100644 index 0000000000..890101d48a --- /dev/null +++ b/lib/src/components/WuiProvider/styles.ts @@ -0,0 +1,404 @@ +import { createGlobalStyle, css, th } from '@xstyled/styled-components' + +import { fonts } from './font' + +/* stylelint-disable */ +export const resetStyles = css` + html, + body, + div, + span, + applet, + object, + iframe, + h1, + h2, + h3, + h4, + h5, + h6, + p, + blockquote, + pre, + a, + abbr, + acronym, + address, + big, + cite, + code, + del, + dfn, + em, + img, + ins, + kbd, + q, + s, + samp, + small, + strike, + strong, + sub, + sup, + tt, + var, + b, + u, + i, + center, + dl, + dt, + dd, + ol, + ul, + li, + fieldset, + form, + label, + legend, + table, + caption, + tbody, + tfoot, + thead, + tr, + th, + td, + article, + aside, + canvas, + details, + embed, + figure, + figcaption, + footer, + header, + hgroup, + menu, + nav, + output, + ruby, + section, + summary, + time, + mark, + audio, + video { + min-width: 0; + min-height: 0; + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + vertical-align: baseline; + } + article, + aside, + details, + figcaption, + figure, + footer, + header, + hgroup, + menu, + nav, + section { + display: block; + } + ol, + ul { + list-style: none; + } + blockquote, + q { + quotes: none; + } + blockquote::before, + blockquote::after, + q::before, + q::after { + content: ''; + content: none; + } + table { + border-collapse: collapse; + border-spacing: 0; + } + a { + text-decoration: none; + } + img { + overflow: hidden; + } + input { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + } + input::-webkit-search-cancel-button { + display: none; + } + :focus { + outline: none !important; /* important for firefox */ + } + *, + *::after, + *::before { + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + html { + height: 100%; + } + body { + min-height: 100%; + padding-top: 1px; + margin-top: -1px; + } +` + +export const normalizeStyle = css` + html { + line-height: 1.15; + -webkit-text-size-adjust: 100%; + } + + body { + margin: 0; + } + + main { + display: block; + } + + h1 { + font-size: 2em; + margin: 0.67em 0; + } + + hr { + box-sizing: content-box; + height: 0; + overflow: visible; + } + + pre { + font-family: monospace, monospace; + font-size: 1em; + } + + a { + background-color: transparent; + } + + abbr[title] { + border-bottom: none; + text-decoration: underline; + text-decoration: underline dotted; + } + + b, + strong { + font-weight: bolder; + } + + code, + kbd, + samp { + font-family: monospace, monospace; + font-size: 1em; + } + + small { + font-size: 80%; + } + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + img { + border-style: none; + } + + button, + input, + optgroup, + select, + textarea { + font-family: inherit; + font-size: 100%; + line-height: 1.15; + margin: 0; + } + + button, + input { + overflow: visible; + } + + button, + select { + text-transform: none; + } + + button, + [type='button'], + [type='reset'], + [type='submit'] { + -webkit-appearance: button; + } + + button::-moz-focus-inner, + [type='button']::-moz-focus-inner, + [type='reset']::-moz-focus-inner, + [type='submit']::-moz-focus-inner { + border-style: none; + padding: 0; + } + + button:-moz-focusring, + [type='button']:-moz-focusring, + [type='reset']:-moz-focusring, + [type='submit']:-moz-focusring { + outline: 1px dotted ButtonText; + } + + fieldset { + padding: 0.35em 0.75em 0.625em; + } + + legend { + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal; + } + + progress { + vertical-align: baseline; + } + + textarea { + overflow: auto; + } + + [type='checkbox'], + [type='radio'] { + box-sizing: border-box; + padding: 0; + } + + [type='number']::-webkit-inner-spin-button, + [type='number']::-webkit-outer-spin-button { + height: auto; + } + + [type='search'] { + -webkit-appearance: textfield; + outline-offset: -2px; + } + + [type='search']::-webkit-search-decoration { + -webkit-appearance: none; + } + + ::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; + } + + details { + display: block; + } + + summary { + display: list-item; + } + + template { + display: none; + } + + [hidden] { + display: none; + } +` +/* stylelint-enable */ + +const baseBoxSizing = css` + * { + &, + &::before, + &::after { + box-sizing: border-box; + } + } +` + +function baseFonts() { + return css` + body, + button, + input, + select, + textarea { + /* stylelint-disable-next-line */ + font-family: texts; + -webkit-font-smoothing: antialiased; + line-height: html; + letter-spacing: html; + } + ` +} + +export const GlobalStyle = createGlobalStyle<{ useReset?: boolean }>( + ({ useReset }) => css` + ${normalizeStyle}; + ${fonts()}; + ${baseFonts()}; + ${useReset ? resetStyles : baseBoxSizing}; + + html { + color: neutral-60; + } + + ::selection { + ${th('selection')}; + } + + /* for firefox */ + &[type='search'] { + appearance: none; + } + + /* to remove x on macos */ + input[type='search']::-webkit-search-decoration, + input[type='search']::-webkit-search-cancel-button, + input[type='search']::-webkit-search-results-button, + input[type='search']::-webkit-search-results-decoration { + appearance: none; + } + + /* Fix to toast notification when useReset prop is add to WUI provider */ + .Toaster__message-wrapper { + min-height: 'auto'; + } + ` +) diff --git a/lib/src/components/index.tsx b/lib/src/components/index.tsx new file mode 100644 index 0000000000..b1ee13487c --- /dev/null +++ b/lib/src/components/index.tsx @@ -0,0 +1,3 @@ +export * from './Box' +export * from './System' +export * from './WuiProvider' diff --git a/lib/src/index.tsx b/lib/src/index.tsx new file mode 100644 index 0000000000..574e5cd825 --- /dev/null +++ b/lib/src/index.tsx @@ -0,0 +1,2 @@ +export * from './theme' +export * from './components' diff --git a/lib/src/theme/borders.ts b/lib/src/theme/borders.ts new file mode 100644 index 0000000000..58b4fb18f1 --- /dev/null +++ b/lib/src/theme/borders.ts @@ -0,0 +1,11 @@ +export type ThemeBorderWidths = { + lg: string + md: string + sm: string +} + +export const borderWidths: ThemeBorderWidths = { + sm: '1px', + md: '2px', + lg: '3px', +} diff --git a/lib/src/theme/colors.ts b/lib/src/theme/colors.ts new file mode 100644 index 0000000000..bf1768df42 --- /dev/null +++ b/lib/src/theme/colors.ts @@ -0,0 +1,133 @@ +const palette = { + 'beige-10': '#FBF9F7', + 'beige-20': '#F6F3EF', + 'beige-30': '#EAE4DE', + 'beige-40': '#D2CBC3', + 'beige-50': '#A7A096', + 'beige-60': '#605B55', + 'beige-70': '#4D4944', + 'beige-80': '#33302D', + 'beige-90': '#1E1C1A', + 'blue-10': '#E0F5FF', + 'blue-20': '#BBEAFF', + 'blue-30': '#9BDEF7', + 'blue-40': '#55C3E9', // brand + 'blue-50': '#27A5D0', + 'blue-60': '#057AA3', + 'blue-70': '#025A79', + 'blue-80': '#013C50', + 'blue-90': '#00202B', + 'green-10': '#EAFFD4', + 'green-20': '#D6F6B4', + 'green-30': '#BADE94', // brand + 'green-40': '#9FC873', + 'green-50': '#83AD57', + 'green-60': '#5A8034', + 'green-70': '#40611F', + 'green-80': '#2A4210', + 'green-90': '#142603', + 'neutral-10': '#FFFFFF', + 'neutral-20': '#F3F3F3', + 'neutral-30': '#DEDEDE', + 'neutral-40': '#BDBDBD', + 'neutral-50': '#989898', + 'neutral-60': '#585858', + 'neutral-70': '#444444', + 'neutral-80': '#212121', + 'neutral-90': '#000000', + 'orange-10': '#FFEBCE', + 'orange-20': '#FFD495', + 'orange-30': '#FFBB59', + 'orange-40': '#FF9F14', + 'orange-50': '#DB860A', + 'orange-60': '#A6670A', + 'orange-70': '#824F06', + 'orange-80': '#573607', + 'orange-90': '#382303', + 'pink-10': '#FFEAF5', + 'pink-20': '#FFD5EB', + 'pink-30': '#FEB7DC', + 'pink-40': '#F696C8', // brand + 'pink-50': '#E468A8', + 'pink-60': '#C24887', + 'pink-70': '#972962', + 'pink-80': '#6D1142', + 'pink-90': '#3C0725', + 'red-10': '#FBDEDC', + 'red-20': '#FCC7C3', + 'red-30': '#FDB3AE', + 'red-40': '#FF9490', + 'red-50': '#FF6165', + 'red-60': '#E1003A', + 'red-70': '#A80029', + 'red-80': '#75001A', + 'red-90': '#450101', + 'red-orange-10': '#FFDED0', + 'red-orange-20': '#FFC9B2', + 'red-orange-30': '#FFB595', + 'red-orange-40': '#FF9868', // brand + 'red-orange-50': '#E67B49', + 'red-orange-60': '#C45927', + 'red-orange-70': '#9F4217', + 'red-orange-80': '#6D2605', + 'red-orange-90': '#451701', + 'teal-10': '#D5FFFA', + 'teal-20': '#AAF4EB', + 'teal-30': '#6DE1D2', + 'teal-40': '#00C7AE', // brand + 'teal-50': '#01AA95', + 'teal-60': '#008070', + 'teal-70': '#00544A', + 'teal-80': '#003D35', + 'teal-90': '#00211D', + 'violet-10': '#F2F2FF', + 'violet-20': '#E0E0FF', + 'violet-30': '#C9C9FF', + 'violet-40': '#ACACFF', // brand + 'violet-50': '#9080F0', + 'violet-60': '#7958D6', + 'violet-70': '#593CAC', + 'violet-80': '#422A86', + 'violet-90': '#1F0E51', + 'yellow-10': '#FFF8D9', + 'yellow-20': '#FFF1B2', + 'yellow-30': '#FFE166', + 'yellow-40': '#FFCD00', + 'yellow-50': '#E5B800', + 'yellow-60': '#B69200', + 'yellow-70': '#846A01', + 'yellow-80': '#604D00', + 'yellow-90': '#423500', +} + +export const getColors = (systemColors: typeof palette) => ({ + ...systemColors, + 'primary-10': systemColors['yellow-10'], + 'primary-20': systemColors['yellow-20'], + 'primary-30': systemColors['yellow-30'], + 'primary-40': systemColors['yellow-40'], + 'primary-50': systemColors['yellow-50'], + 'primary-60': systemColors['yellow-60'], + 'primary-70': systemColors['yellow-70'], + 'primary-80': systemColors['yellow-80'], + 'primary-90': systemColors['yellow-90'], + 'secondary-blue': systemColors['blue-40'], + 'secondary-green': systemColors['green-30'], + 'secondary-orange': systemColors['red-orange-40'], + 'secondary-pink': systemColors['pink-40'], + 'secondary-teal': systemColors['teal-40'], + 'secondary-violet': systemColors['violet-40'], + overlay: 'rgba(0, 0, 0, 0.55)', +}) + +export const colors = getColors(palette) +export type ThemeColors = typeof colors +const enum SecondaryColors { + 'blue', + 'green', + 'orange', + 'pink', + 'teal', + 'violet', +} +export type ThemeSecondaryColors = keyof typeof SecondaryColors diff --git a/lib/src/theme/dark.ts b/lib/src/theme/dark.ts new file mode 100644 index 0000000000..135267c907 --- /dev/null +++ b/lib/src/theme/dark.ts @@ -0,0 +1,31 @@ +import { colors, getColors } from './colors' + +import { ThemeValues } from '.' + +type RecursivePartial = { + [P in keyof T]?: T[P] | RecursivePartial +} + +/** + * We reverse the palette colors (basic token color) and inject this new values on dark theme + */ +const generateDarkColors = Object.keys(colors).reduce((acc, key) => { + if (key.startsWith('secondary-') || key === 'overlay') return acc + + const number = 100 - Number(key.slice(-2)) + const variant = key.slice(0, key.length - 2) + + return { + ...acc, + [key]: colors[`${variant}${number}` as keyof ThemeValues['colors']], + } +}, {}) + +export const colorsDark: ThemeValues['colors'] = { + ...colors, + ...getColors(generateDarkColors as ThemeValues['colors']), +} + +export const darkTheme: RecursivePartial = { + colors: colorsDark, +} diff --git a/lib/src/theme/focus.ts b/lib/src/theme/focus.ts new file mode 100644 index 0000000000..6471d4e2a0 --- /dev/null +++ b/lib/src/theme/focus.ts @@ -0,0 +1,17 @@ +import { CSSObject } from '@xstyled/styled-components' + +import { ThemeColors } from './colors' + +export type ThemeFocus = (color?: string) => { + boxShadow: CSSObject['boxShadow'] +} + +export const getFocus = ({ colors }: { colors: ThemeColors }) => { + function focus(color = colors['primary-40']) { + return { + boxShadow: `0 0 0 2px ${color}`, + } + } + + return focus +} diff --git a/lib/src/theme/fonts.ts b/lib/src/theme/fonts.ts new file mode 100644 index 0000000000..2ba6d54309 --- /dev/null +++ b/lib/src/theme/fonts.ts @@ -0,0 +1,64 @@ +import { ThemeValues } from '.' + +type FontFace = { + display?: FontDisplay + extensions?: string[] + isVariable?: boolean + stretch?: string + style?: string + uniCodeRange?: string + url: string + weight?: string +} + +export type ThemeFontFaces = { + 'welcome-font': FontFace[] + 'welcome-icon-font': FontFace[] + 'work-sans': FontFace[] +} + +export const fontFaces = (theme: ThemeValues): ThemeFontFaces => ({ + 'welcome-font': [ + { + url: `${theme.fontsUrl}/welcome-font-regular`, + weight: '400', + }, + { + url: `${theme.fontsUrl}/welcome-font-medium`, + weight: '500', + }, + { + url: `${theme.fontsUrl}/welcome-font-bold`, + weight: '600', + }, + { + url: `${theme.fontsUrl}/welcome-font-regular-italic`, + style: 'italic', + weight: '400', + }, + { + url: `${theme.fontsUrl}/welcome-font-medium-italic`, + style: 'italic', + weight: '500', + }, + { + url: `${theme.fontsUrl}/welcome-font-bold-italic`, + style: 'italic', + weight: '600', + }, + ], + 'welcome-icon-font': [ + { + url: `${theme.fontsUrl}/icon-font/__ICON_FONT_HASH__/welcome-icon-font`, + display: 'block', + }, + ], + 'work-sans': [ + { + url: `${theme.fontsUrl}/work-sans-variable`, + isVariable: true, + stretch: '75% 125%', + weight: '100 1000', + }, + ], +}) diff --git a/lib/src/theme/index.ts b/lib/src/theme/index.ts new file mode 100644 index 0000000000..e9578b0536 --- /dev/null +++ b/lib/src/theme/index.ts @@ -0,0 +1,193 @@ +import { + CSSScalar, + defaultTheme, + rpxTransformers, + ITheme as StyledComponentDefaultTheme, + DefaultTheme as XStyledDefaultTheme, +} from '@xstyled/styled-components' + +import { mergeDeepRight } from '../../utils/mergeDeepRight' + +import { darkTheme } from './dark' +import { colors, ThemeColors } from './colors' +import { fontFaces, ThemeFontFaces } from './fonts' +import { + fontWeights, + getFonts, + getFontSizes, + getLetterSpacings, + getLineHeights, + getTextFontColors, + getTexts, + getTextsFontFamily, + getTextsFontWeights, + getTextsTextTransform, + ThemeFonts, + ThemeFontSizes, + ThemeFontWeights, + ThemeLetterSpacings, + ThemeLineHeights, + ThemeTexts, + ThemeTextsFontFamily, + ThemeTextsFontWeights, + ThemeTextsTextTransform, +} from './typography' +import { ThemeTimingFunction, ThemeTransitions, timingFunction, transitions } from './transitions' +import { getUnderline, ThemeUnderline } from './underline' +import { getRadii, ThemeRadii } from './radii' +import { borderWidths, ThemeBorderWidths } from './borders' +import { screens, ThemeScreens } from './screens' +import { shadows, ThemeShadows } from './shadows' +import { getSpace, ThemeSpace } from './space' +import { getSelection, ThemeSelection } from './selection' +import { getFocus, ThemeFocus } from './focus' + +const DEFAULT_FONT_FAMILY = 'work-sans' +const DEFAULT_FONT_SIZE = 16 +const DEFAULT_LETTER_SPACING = '-0.019rem' +const DEFAULT_LINE_HEIGHT = 1.15 +const FONTS_URL = 'https://cdn.welcome-ui.com/fonts' +const HEADING_FONT_FAMILY = 'welcome-font' +const ICON_FONT_FAMILY = 'welcome-icon-font' + +type OverrideKeys = + | 'colors' + | 'radii' + | 'borderWidths' + | 'fontSizes' + | 'lineHeights' + | 'fontWeights' + | 'letterSpacings' + | 'fonts' + | 'sizes' + | 'screens' + | 'space' + | 'shadows' + | 'texts' + +type XStyledTheme = Omit +type StyledComponentsTheme = Omit + +export interface ThemeValues extends XStyledTheme, StyledComponentsTheme { + borderWidths: ThemeBorderWidths + colors: ThemeColors + defaultLetterSpacing: string + defaultLineHeight: number + focus: ThemeFocus + fontFaces: ThemeFontFaces + fontSizes: ThemeFontSizes + fontWeights: ThemeFontWeights + fonts: ThemeFonts + fontsUrl: ThemeFontsUrl + inset: ThemeSpace + letterSpacings: ThemeLetterSpacings + lineHeights: ThemeLineHeights + radii: ThemeRadii + screens: ThemeScreens + selection: ThemeSelection + shadows: ThemeShadows + space: ThemeSpace + texts: ThemeTexts + textsFontFamily: ThemeTextsFontFamily + textsFontWeights: ThemeTextsFontWeights + textsTextTransform: ThemeTextsTextTransform + timingFunction: ThemeTimingFunction + toEm: (int: number) => string + toPx: (int: number) => string + toRem: (int: number) => string + transformers: { + border: (value: CSSScalar) => CSSScalar + px: (value: CSSScalar) => CSSScalar + } + transitions: ThemeTransitions + underline: ThemeUnderline +} + +export type ThemeFontsUrl = + | 'https://cdn.welcome-ui.com/fonts' + | 'https://cdn.welcometothejungle.com/fonts' + | string + +export type Options = { + [param: string]: unknown + defaultFontFamily?: string + defaultFontSize?: number + defaultLetterSpacing?: string + defaultLineHeight?: number + fontsUrl?: ThemeFontsUrl + headingFontFamily?: string + iconFontFamily?: string +} + +export const createTheme = (options: Options = {}): ThemeValues => { + const { + defaultFontFamily = DEFAULT_FONT_FAMILY, + defaultFontSize = DEFAULT_FONT_SIZE, + defaultLetterSpacing = DEFAULT_LETTER_SPACING, + defaultLineHeight = DEFAULT_LINE_HEIGHT, + fontsUrl = FONTS_URL, + headingFontFamily = HEADING_FONT_FAMILY, + iconFontFamily = ICON_FONT_FAMILY, + ...rest + } = options + + let theme = {} as ThemeValues + + theme.transformers = { ...rpxTransformers } + + theme.toEm = px => `${px / defaultFontSize}em` + theme.toRem = px => `${px / defaultFontSize}rem` + theme.toPx = rem => `${rem * defaultFontSize}px` + + theme.colors = colors + + // fonts + theme.fontsUrl = fontsUrl + theme.fontFaces = fontFaces(theme) + theme.fontSizes = getFontSizes('rem', theme) + theme.defaultLineHeight = defaultLineHeight as number + theme.defaultLetterSpacing = defaultLetterSpacing as string + theme.lineHeights = getLineHeights(theme) + theme.fontWeights = fontWeights + theme.letterSpacings = getLetterSpacings(theme) + theme.fonts = getFonts(defaultFontFamily, headingFontFamily, iconFontFamily) + theme.borderWidths = borderWidths + + theme.screens = screens + + theme.space = getSpace(theme) + + theme.inset = theme.space + + theme.radii = getRadii(theme) + + theme.transitions = transitions + theme.timingFunction = timingFunction + + theme.shadows = shadows + + theme = mergeDeepRight(theme, rest) as ThemeValues + + // These attributes depend on colors and fontSizes and must come last + theme.selection = getSelection(theme) + theme.underline = getUnderline(theme) + theme.focus = getFocus(theme) + theme.textsFontWeights = getTextsFontWeights(theme) + theme.textsFontFamily = getTextsFontFamily(theme) + theme.textsFontColors = getTextFontColors(theme) + theme.textsTextTransform = getTextsTextTransform() + theme.texts = getTexts(theme) + + // components + + // fields + + // states + theme.states = defaultTheme.states + + theme = mergeDeepRight(theme, rest) as ThemeValues + + return theme +} + +export { darkTheme } diff --git a/lib/src/theme/radii.ts b/lib/src/theme/radii.ts new file mode 100644 index 0000000000..976b3304de --- /dev/null +++ b/lib/src/theme/radii.ts @@ -0,0 +1,24 @@ +import { ThemeValues } from '.' + +export type ThemeRadii = { + [key: number]: string + full: string + lg: string + md: string + none: string + sm: string + xl: string + xxl: string +} + +export const getRadii = (theme: ThemeValues): ThemeRadii => { + return { + none: '0', + sm: theme.toRem(2), + md: theme.toRem(4), + lg: theme.toRem(8), + xl: theme.toRem(16), + xxl: theme.toRem(24), + full: '100%', + } +} diff --git a/lib/src/theme/screens.ts b/lib/src/theme/screens.ts new file mode 100644 index 0000000000..4a72dceec8 --- /dev/null +++ b/lib/src/theme/screens.ts @@ -0,0 +1,23 @@ +export type ThemeScreens = { + [key: string]: number + [key: number]: number + '3xl': number + '4xl': number + lg: number + md: number + sm: number + xl: number + xs: number + xxl: number +} + +export const screens: ThemeScreens = { + xs: 0, + sm: 480, + md: 736, + lg: 980, + xl: 1280, + xxl: 1440, + '3xl': 1620, + '4xl': 1920, +} diff --git a/lib/src/theme/selection.ts b/lib/src/theme/selection.ts new file mode 100644 index 0000000000..8308de19f8 --- /dev/null +++ b/lib/src/theme/selection.ts @@ -0,0 +1,12 @@ +import { CSSObject } from '@xstyled/styled-components' + +import { ThemeValues } from '.' + +export type ThemeSelection = CSSObject + +export const getSelection = (theme: ThemeValues): ThemeSelection => { + return { + backgroundColor: theme.colors['primary-40'], + color: theme.colors['neutral-90'], + } +} diff --git a/lib/src/theme/shadows.ts b/lib/src/theme/shadows.ts new file mode 100644 index 0000000000..f1844dddb5 --- /dev/null +++ b/lib/src/theme/shadows.ts @@ -0,0 +1,9 @@ +export type ThemeShadows = { + md: string + sm: string +} + +export const shadows: ThemeShadows = { + sm: '1px 2px 4px 0 rgba(0,0,0,0.05)', + md: '3px 4px 10px 0 rgba(0,0,0,0.07)', +} diff --git a/lib/src/theme/space.ts b/lib/src/theme/space.ts new file mode 100644 index 0000000000..48fdb240e8 --- /dev/null +++ b/lib/src/theme/space.ts @@ -0,0 +1,35 @@ +import { ThemeValues } from '.' + +export type ThemeSpace = { + [key: string]: string + [key: number]: string + '3xl': string + '4xl': string + '5xl': string + '6xl': string + '7xl': string + lg: string + md: string + sm: string + xl: string + xs: string + xxl: string + xxs: string +} + +export const getSpace = (theme: ThemeValues): ThemeSpace => { + return { + xxs: theme.toRem(2), + xs: theme.toRem(4), + sm: theme.toRem(8), + md: theme.toRem(12), + lg: theme.toRem(16), + xl: theme.toRem(24), + xxl: theme.toRem(32), + '3xl': theme.toRem(48), + '4xl': theme.toRem(64), + '5xl': theme.toRem(96), + '6xl': theme.toRem(128), + '7xl': theme.toRem(192), + } +} diff --git a/lib/src/theme/transitions.ts b/lib/src/theme/transitions.ts new file mode 100644 index 0000000000..c334eaa9fe --- /dev/null +++ b/lib/src/theme/transitions.ts @@ -0,0 +1,25 @@ +import { CSSObject } from '@xstyled/styled-components' + +export type ThemeTimingFunction = { + primary: CSSObject['transition-timing-function'] + secondary: CSSObject['transition-timing-function'] + tertiary: CSSObject['transition-timing-function'] +} + +export const timingFunction: ThemeTimingFunction = { + primary: 'ease', + secondary: 'linear', + tertiary: 'cubic-bezier(0.41, 0.094, 0.54, 0.07)', +} + +export type ThemeTransitions = { + fast: CSSObject['transition'] + medium: CSSObject['transition'] + slow: CSSObject['transition'] +} + +export const transitions: ThemeTransitions = { + slow: `500ms ${timingFunction.tertiary}`, + medium: `300ms ${timingFunction.primary}`, + fast: `100ms ${timingFunction.secondary}`, +} diff --git a/lib/src/theme/typography.ts b/lib/src/theme/typography.ts new file mode 100644 index 0000000000..302cc2a465 --- /dev/null +++ b/lib/src/theme/typography.ts @@ -0,0 +1,301 @@ +import { CSSObject } from '@xstyled/styled-components' + +import { Options, ThemeValues } from '.' + +export type ThemeFontSizes = { + [key: number]: string + h0: string + h1: string + h2: string + h3: string + h4: string + h5: string + h6: string + lg: string + md: string + sm: string + 'subtitle-md': string + 'subtitle-sm': string + xs: string +} + +export const getFontSizes = (unit: string, theme: ThemeValues): ThemeFontSizes => { + const { toEm, toRem } = theme + const convert = unit === 'em' ? toEm : toRem + + return { + h0: convert(65), + h1: convert(45), + h2: convert(36), + h3: convert(26), + h4: convert(20), + h5: convert(16), + h6: convert(14), + lg: convert(18), + md: convert(16), + sm: convert(14), + 'subtitle-md': convert(13), + 'subtitle-sm': convert(11), + xs: convert(12), + } +} + +export type ThemeLineHeights = { + [key: number]: number | string + h0: number | string + h1: number | string + h2: number | string + h3: number | string + h4: number | string + h5: number | string + h6: number | string + html: number | string + lg: number | string + md: number | string + sm: number | string + 'subtitle-md': number | string + 'subtitle-sm': number | string + xs: number | string +} + +export const getLineHeights = ({ + defaultLineHeight, + toRem, +}: { + defaultLineHeight: number + toRem: (value: number) => string +}): ThemeLineHeights => { + return { + html: defaultLineHeight, + h0: toRem(72), + h1: toRem(48), + h2: toRem(40), + h3: toRem(32), + h4: toRem(24), + h5: toRem(18), + h6: toRem(16), + lg: toRem(24), + md: toRem(18), + sm: toRem(18), + xs: toRem(14), + 'subtitle-md': defaultLineHeight, + 'subtitle-sm': defaultLineHeight, + } +} + +export type ThemeFontWeights = { + [key: string]: number + bold: number + medium: number + regular: number +} + +export const fontWeights: ThemeFontWeights = { + regular: 400, + medium: 500, + bold: 600, +} + +export type ThemeLetterSpacings = { + [key: string]: string + h0: string + h1: string + h2: string + h3: string + h4: string + h5: string + h6: string + html: string + lg: string + md: string + sm: string + 'subtitle-md': string + 'subtitle-sm': string + xs: string +} + +export const getLetterSpacings = ({ + defaultLetterSpacing, + toRem, +}: { + defaultLetterSpacing: string + toRem: (value: number) => string +}): ThemeLetterSpacings => { + return { + html: defaultLetterSpacing, + h0: toRem(-1.7), + h1: toRem(-1.2), + h2: toRem(-1), + h3: toRem(-0.9), + h4: toRem(-0.6), + h5: toRem(-0.5), + h6: toRem(-0.5), + lg: defaultLetterSpacing, + md: defaultLetterSpacing, + sm: defaultLetterSpacing, + xs: toRem(-0.2), + 'subtitle-md': toRem(-0.2), + 'subtitle-sm': toRem(-0.2), + } +} + +export type ThemeTextsFontWeights = { + [key: string]: number + h0: number + h1: number + h2: number + h3: number + h4: number + h5: number + h6: number + lg: number + md: number + sm: number + 'subtitle-md': number + 'subtitle-sm': number + xs: number +} + +export const getTextsFontWeights = (theme: ThemeValues): ThemeTextsFontWeights => { + const { fontWeights } = theme + + return { + h0: fontWeights.bold, + h1: fontWeights.bold, + h2: fontWeights.bold, + h3: fontWeights.bold, + h4: fontWeights.bold, + h5: fontWeights.bold, + h6: fontWeights.bold, + lg: fontWeights.regular, + md: fontWeights.regular, + sm: fontWeights.regular, + 'subtitle-md': fontWeights.bold, + 'subtitle-sm': fontWeights.medium, + xs: fontWeights.regular, + } +} + +export type ThemeTextsFontFamily = { + [key: string]: string + h0: string + h1: string + h2: string + h3: string + h4: string + h5: string + h6: string + 'subtitle-md': string + 'subtitle-sm': string +} + +export const getTextsFontFamily = (theme: ThemeValues): ThemeTextsFontFamily => { + const { fonts } = theme + + return { + h0: fonts.headings, + h1: fonts.headings, + h2: fonts.headings, + h3: fonts.headings, + h4: fonts.headings, + h5: fonts.headings, + h6: fonts.headings, + 'subtitle-md': fonts.headings, + 'subtitle-sm': fonts.headings, + } +} + +export type ThemeTextsTextTransform = { + [key: string]: string + 'subtitle-md': string + 'subtitle-sm': string +} + +export const getTextsTextTransform = (): ThemeTextsTextTransform => { + return { + 'subtitle-md': 'uppercase', + 'subtitle-sm': 'uppercase', + } +} + +export type ThemeTextsFontColors = { + [key: number]: string + h0: string + h1: string + h2: string + h3: string + h4: string + h5: string + h6: string +} + +export const getTextFontColors = (theme: ThemeValues): ThemeTextsFontColors => { + const { colors } = theme + + return { + h0: colors['neutral-90'], + h1: colors['neutral-90'], + h2: colors['neutral-90'], + h3: colors['neutral-90'], + h4: colors['neutral-90'], + h5: colors['neutral-90'], + h6: colors['neutral-90'], + } +} + +export type ThemeTexts = { + [key: string]: Partial<{ + color: CSSObject['color'] + fontFamily: CSSObject['fontFamily'] + fontSize: CSSObject['fontSize'] + fontWeight: CSSObject['fontWeight'] + letterSpacing: CSSObject['letterSpacing'] + lineHeight: CSSObject['lineHeight'] + textTransform: CSSObject['textTransform'] + }> +} + +export const getTexts = (theme: ThemeValues): ThemeTexts => { + const { + fontSizes, + letterSpacings, + lineHeights, + textsFontColors, + textsFontFamily, + textsFontWeights, + textsTextTransform, + } = theme + + return Object.keys(fontSizes).reduce((acc, key) => { + return { + ...acc, + [key]: { + color: textsFontColors[key as keyof ThemeTextsFontColors], + fontFamily: textsFontFamily[key as keyof ThemeTextsFontFamily] || undefined, + fontWeight: textsFontWeights[key as keyof ThemeTextsFontFamily], + fontSize: fontSizes[key as keyof ThemeFontSizes], + lineHeight: lineHeights[key as keyof ThemeLineHeights] || lineHeights.lg, + letterSpacing: letterSpacings[key as keyof ThemeLetterSpacings] || undefined, + textTransform: textsTextTransform[key as keyof ThemeTextsTextTransform] || undefined, + }, + } + }, {}) +} + +export type ThemeFonts = { + headings: string + icons: string + texts: string +} + +export const getFonts = ( + defaultFontFamily: Options['defaultFontFamily'], + headingFontFamily: Options['headingFontFamily'], + iconFontFamily: Options['iconFontFamily'] +): ThemeFonts => { + return { + texts: [defaultFontFamily, 'sans-serif'].join(', '), + headings: [headingFontFamily, 'sans-serif'].join(', '), + icons: iconFontFamily, + } +} diff --git a/lib/src/theme/underline.ts b/lib/src/theme/underline.ts new file mode 100644 index 0000000000..b9bff04f6a --- /dev/null +++ b/lib/src/theme/underline.ts @@ -0,0 +1,32 @@ +import { css } from '@xstyled/styled-components' + +import { ThemeColors } from './colors' + +export type ThemeUnderline = { + default: ReturnType + hover: ReturnType +} + +export const getUnderline = ({ colors }: { colors: ThemeColors }): ThemeUnderline => { + return { + default: css` + background-image: linear-gradient( + 0deg, + ${colors['primary-40']}, + ${colors['primary-40']} 100% + ); + background-repeat: no-repeat; + background-size: 100% 50%; + background-position-y: calc(200% - 2px); + transition: + background-position-y 250ms, + background-size 250ms, + color 250ms; + `, + hover: css` + opacity: 1; + background-position-y: 100%; + background-size: 100% 100%; + `, + } +} diff --git a/lib/stylelint.config.js b/lib/stylelint.config.js new file mode 100644 index 0000000000..30f32936ef --- /dev/null +++ b/lib/stylelint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('wttj-config/lib/stylelint')], +} diff --git a/lib/tests/index.tsx b/lib/tests/index.tsx new file mode 100644 index 0000000000..d6e8d7bf23 --- /dev/null +++ b/lib/tests/index.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { RenderOptions, render as rtlRender } from '@testing-library/react' +import { ThemeProvider } from '@xstyled/styled-components' +import userEvent, { UserEvent } from '@testing-library/user-event' +import { BrowserRouter } from 'react-router-dom' + +import '@testing-library/jest-dom' +import 'jest-styled-components' + +import { createTheme } from '../src' + +type ProviderProps = { + children?: React.ReactNode +} + +const Provider: React.FC = ({ children }) => { + const theme = createTheme() + + return ( + + {children} + + ) +} + +type RenderResult = ReturnType & { user: UserEvent } + +const customRender = (ui: JSX.Element, options?: RenderOptions): RenderResult => { + const renderResult = rtlRender(ui, { wrapper: Provider, ...options }) + + return { + user: userEvent.setup(), + ...renderResult, + } +} + +// override render method +export { customRender as render } diff --git a/lib/tsconfig.json b/lib/tsconfig.json new file mode 100644 index 0000000000..9000ea0568 --- /dev/null +++ b/lib/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "wttj-config/lib/tsconfig/tsconfig.json", + "compilerOptions": { + "outDir": ".", + "baseUrl": "./lib/src", + "sourceMap": false, + "declaration": true, + "target": "ES6" + }, + "include": ["src"], + "exclude": ["**/docs/**"] +} diff --git a/lib/utils/mergeDeepRight.ts b/lib/utils/mergeDeepRight.ts new file mode 100644 index 0000000000..22fe19e0fd --- /dev/null +++ b/lib/utils/mergeDeepRight.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-prototype-builtins */ +export function mergeDeepRight, U extends Record>( + obj1: T, + obj2: U +): T & U { + if (typeof obj1 !== 'object' || obj1 === null) return obj2 as T & U + if (typeof obj2 !== 'object' || obj2 === null) return obj1 as T & U + + const result: Record = { ...obj1 } + + for (const key in obj2) { + if (obj2.hasOwnProperty(key)) { + result[key] = mergeDeepRight(result[key], obj2[key]) + } + } + + return result as T & U +} diff --git a/lib/vite.config.mjs b/lib/vite.config.mjs new file mode 100644 index 0000000000..ad89be12ee --- /dev/null +++ b/lib/vite.config.mjs @@ -0,0 +1,47 @@ +import { defineConfig } from 'vite' +import dts from 'vite-plugin-dts' +import preserveDirectives from 'rollup-preserve-directives' + +import { generateWebsiteExamplesPlugin } from './scripts/generate-website-examples' +import { getComponentsEntries } from './scripts/get-components-entry' +import { generateTypesDoc } from './scripts/generate-types-doc' + +function addUseClientDirectivePlugin() { + return { + name: 'add-use-client', + transform(code) { + // Only add for client-side files (adjust the filter as needed) + return { + code: `'use client';\n${code}`, + map: null, + } + }, + } +} + +export default defineConfig({ + build: { + banner: "'use client';", + lib: { + entry: getComponentsEntries(), + fileName: '[name]', + name: 'Welcome UI', + }, + rollupOptions: { + external: ['react', '@xstyled/styled-components'], + output: { + globals: { + react: 'React', + '@xstyled/styled-components': 'XStyled', + }, + }, + }, + }, + plugins: [ + preserveDirectives(), + addUseClientDirectivePlugin(), + generateWebsiteExamplesPlugin(), + generateTypesDoc(), + dts({ outDir: 'dist/types', entryRoot: 'src', exclude: ['**/*.test*ts*'] }), + ], +}) diff --git a/migrated_packages.ts b/migrated_packages.ts new file mode 100644 index 0000000000..aa809e1356 --- /dev/null +++ b/migrated_packages.ts @@ -0,0 +1 @@ +export const MIGRATED_PACKAGES = ['Box'] diff --git a/package.json b/package.json index faedc44199..88c27fdab7 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build:packages": "yarn lerna run build --ignore @welcome-ui/core", "build:types": "yarn lerna run types", "build": "yarn webfont:build && yarn build:core && yarn build:packages && yarn build:types && yarn build:docs", + "build:monorepo": "cd lib && yarn build", "check:audit": "/bin/bash -c 'yarn audit --level critical; [[ $? -ge 16 ]] && exit 1 || exit 0'", "check:deps": "yarn lerna exec --no-bail --stream depcheck", "clean": "yarn lerna clean", @@ -34,6 +35,7 @@ "release": "yarn lerna version --conventional-commits --no-private", "dev:prerelease": "yarn lerna version --no-private --preid alpha", "start": "yarn website:dev", + "start:monorepo": "cd lib && yarn start", "test": "yarn jest", "watch": "onchange 'packages/**/*.ts*' -e '**/dist/**' -- node -r esm scripts/watch.js {{changed}}", "webfont:build": "node -r esm scripts/webfont-build.js --force && yarn build:packages --scope @welcome-ui/icons.font", @@ -47,7 +49,8 @@ "icons", "icons/**/*", "../icons/**/*", - "website" + "website", + "lib" ], "repository": { "type": "git", @@ -89,7 +92,6 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", - "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.2", "@types/emoji-mart": "3.0.14", "@types/jest": "^29.5.13", diff --git a/tsconfig.json b/tsconfig.json index 7b71aa8ab0..c0c76bbcd8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,10 +14,5 @@ "packages/**/tests/*.tsx", "types" ], - "exclude": [ - "dist", - "icons/**/*", - "packages/**/node_modules", - "packages/**/dist" - ] -} \ No newline at end of file + "exclude": ["dist", "icons/**/*", "packages/**/node_modules", "packages/**/dist", "lib/**/*"] +} diff --git a/website/app/components/[id]/[subPage]/code.tsx b/website/app/components/[id]/[subPage]/code.tsx index 40b001207e..64e1c214d6 100644 --- a/website/app/components/[id]/[subPage]/code.tsx +++ b/website/app/components/[id]/[subPage]/code.tsx @@ -19,10 +19,10 @@ const Page = ({ params }: PageProps) => { const { id } = params const componentName = getRepository(id) - const { contentWithoutMatter, data, isNotFound, tree } = getPageContent( - `${componentName}/docs/index.mdx`, - true - ) + const { contentWithoutMatter, data, isNotFound, tree } = getPageContent({ + filename: `${componentName}/docs/index.mdx`, + isPackage: true, + }) if (isNotFound) return notFound() @@ -30,7 +30,11 @@ const Page = ({ params }: PageProps) => { <>
{`
`}
- + ## Examples {contentWithoutMatter}
diff --git a/website/app/components/[id]/[subPage]/other.tsx b/website/app/components/[id]/[subPage]/other.tsx index f266e24c1c..7a5b95da12 100644 --- a/website/app/components/[id]/[subPage]/other.tsx +++ b/website/app/components/[id]/[subPage]/other.tsx @@ -16,9 +16,9 @@ export async function generateStaticParams() { const Page = ({ params }: PageProps) => { const { id, subPage } = params - const { contentWithoutMatter, isNotFound, tree } = getPageContent( - `components/${id}/${subPage}.mdx` - ) + const { contentWithoutMatter, isNotFound, tree } = getPageContent({ + filename: `components/${id}/${subPage}.mdx`, + }) if (isNotFound) return notFound() diff --git a/website/app/components/[id]/[subPage]/props.tsx b/website/app/components/[id]/[subPage]/props.tsx index dd3d5f3d4b..07851e230b 100644 --- a/website/app/components/[id]/[subPage]/props.tsx +++ b/website/app/components/[id]/[subPage]/props.tsx @@ -1,3 +1,5 @@ +import { notFound } from 'next/navigation' + import { PageProps } from './page' import { Properties } from '@/build-app/components/Props' @@ -17,7 +19,7 @@ const Page = ({ params }: PageProps) => { const { id } = params const properties = getComponentProperties(getRepository(id)) - if (!properties) return null + if (!properties) return notFound() const tree = getPropertiesTree(properties) diff --git a/website/app/components/[id]/layout.tsx b/website/app/components/[id]/layout.tsx index ce011e7f42..648fbd2abf 100644 --- a/website/app/components/[id]/layout.tsx +++ b/website/app/components/[id]/layout.tsx @@ -1,10 +1,13 @@ import { Text } from '@welcome-ui/text' import { Flex } from '@welcome-ui/flex' import { Button } from '@welcome-ui/button' -import { GithubIcon, NpmIcon } from '@welcome-ui/icons' +import { CheckIcon, GithubIcon } from '@welcome-ui/icons' +import { Tag } from '@welcome-ui/tag' +import { Tooltip } from '@welcome-ui/tooltip' import { Tabs } from './tabs' +import { MIGRATED_PACKAGES } from '@/../migrated_packages' import { Sidebar } from '@/build-app/components/Sidebar' import * as Documentation from '@/build-app/layouts/Documentation' import { getPages } from '@/build-app/utils/pages-components' @@ -21,7 +24,10 @@ type LayoutProps = { export async function generateMetadata({ params }: { params: { [key: string]: string } }) { const { id } = params - const { data } = getPageContent(`${getRepository(id)}/docs/index.mdx`, true) + const { data } = getPageContent({ + filename: `${getRepository(id)}/docs/index.mdx`, + isPackage: true, + }) const title = data?.title const description = data?.description @@ -35,20 +41,30 @@ const Layout = ({ children, params }: LayoutProps) => { const pages = getPages() const { id } = params - const { data } = getPageContent(`${getRepository(id)}/docs/index.mdx`, true) + const { data } = getPageContent({ + filename: `${getRepository(id)}/docs/index.mdx`, + isPackage: true, + }) const title = data?.title const description = data?.description - const packageName = data?.packageName const ariakitLink = data?.ariakit + const isMigratedPackage = title && MIGRATED_PACKAGES.includes(title) + return (
- - {title} - + {isMigratedPackage && ( + + + + Migrated + + + )} + {title} {description && ( {description} @@ -57,7 +73,11 @@ const Layout = ({ children, params }: LayoutProps) => { - {ariakitLink && (