chore: Yarn workspaces + Storybook + WDS (#20776)

This commit is contained in:
Pawan Kumar 2023-03-03 12:17:35 +05:30 committed by GitHub
parent 05e806e2d3
commit 0696183b06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 8130 additions and 828 deletions

View File

@ -14,7 +14,7 @@
"api": "./src/api/",
"assets": "./src/assets/",
"sagas": "./src/sagas/",
"@appsmith": "./src/ee",
"@appsmith": "./src/ee"
}
}]
]

View File

@ -1,5 +1,6 @@
// This JSON file configures the eslint plugin. It supports comments as well as per the JSON5 spec
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["react", "@typescript-eslint", "prettier", "react-hooks","sort-destructure-keys", "cypress"],
"extends": [

View File

@ -48,3 +48,5 @@ build-storybook.log
TODO
/nginx
/public/static/wds/

View File

@ -6,9 +6,9 @@ GIT_SHA=$(eval git rev-parse HEAD)
echo $GIT_SHA
echo "Sentry Auth Token: $SENTRY_AUTH_TOKEN"
# build cra app
REACT_APP_SENTRY_RELEASE=$GIT_SHA REACT_APP_CLIENT_LOG_LEVEL=ERROR EXTEND_ESLINT=true craco --max-old-space-size=4096 build --config craco.build.config.js
if [ "$GITHUB_REPOSITORY" == "appsmithorg/appsmith-ee" ]; then
echo "Deleting sourcemaps for EE"
rm ./build/static/js/*.js.map
@ -16,3 +16,8 @@ if [ "$GITHUB_REPOSITORY" == "appsmithorg/appsmith-ee" ]; then
fi
echo "build finished"
# build storybook and move to the static folder
yarn --cwd packages/storybook build
mv -f ./packages/storybook/storybook-static ./build/storybook

View File

@ -1,4 +1,6 @@
const CracoAlias = require("craco-alias");
const CracoBabelLoader = require("craco-babel-loader");
const path = require("path");
module.exports = {
devServer: {
@ -55,6 +57,12 @@ module.exports = {
tsConfigPath: "./tsconfig.path.json",
},
},
{
plugin: CracoBabelLoader,
options: {
includes: [path.resolve("packages")],
},
},
{
plugin: "prismjs",
options: {

View File

@ -6,6 +6,9 @@
"node": "^16.14.0",
"npm": "^8.5.5"
},
"workspaces": [
"packages/*"
],
"cracoConfig": "craco.dev.config.js",
"dependencies": {
"@blueprintjs/core": "^3.36.0",
@ -35,6 +38,7 @@
"@uppy/react": "^1.11.2",
"@uppy/url": "^1.5.16",
"@uppy/webcam": "^1.8.4",
"@design-system/wds": "*",
"@welldone-software/why-did-you-render": "^4.2.5",
"acorn-walk": "^8.2.0",
"algoliasearch": "^4.2.0",
@ -47,6 +51,7 @@
"core-js": "^3.9.1",
"country-flag-emoji-polyfill": "^0.1.4",
"craco-alias": "^2.1.1",
"craco-babel-loader": "^1.0.4",
"cypress-log-to-output": "^1.1.2",
"dayjs": "^1.10.6",
"deep-diff": "^1.0.2",
@ -186,7 +191,8 @@
"generate:widget": "plop --plopfile generators/index.js",
"postinstall": "patch-package && CURRENT_SCOPE=client node ../shared/install-dependencies.js",
"preinstall": "CURRENT_SCOPE=client node ../shared/build-shared-dep.js",
"install": "node cypress/apply-patches.js"
"install": "node cypress/apply-patches.js",
"storybook": "yarn workspace @design-system/storybook storybook"
},
"browserslist": [
">0.2%",
@ -283,6 +289,7 @@
"patch-package": "^6.4.7",
"plop": "^3.1.1",
"postinstall-postinstall": "^2.1.0",
"prop-types": "^15.8.1",
"raw-loader": "^4.0.2",
"react-is": "^16.12.0",
"react-test-renderer": "^16.11.0",

View File

@ -0,0 +1,16 @@
{
"extends": [
"../../.eslintrc.json",
"plugin:storybook/recommended"
],
"overrides": [
{
"files": [
"**/*.stories.*"
],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
]
}

View File

@ -0,0 +1,136 @@
import styled from "styled-components";
import React, { useEffect } from "react";
import { addons, types } from "@storybook/addons";
import {
Icons,
IconButton,
WithTooltip,
Form,
H6,
ColorControl,
} from "@storybook/components";
import { useGlobals } from "@storybook/api";
import { fontMetricsMap } from "@design-system/wds";
const { Select } = Form;
const Wrapper = styled.div`
padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
`;
const StyledSelect = styled(Select)`
appearance: none;
padding-right: 30px;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23696969%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
background-repeat: no-repeat, repeat;
background-position: right 0.8em top 50%, 0 0;
background-size: 0.65em auto, 100%;
`;
addons.register("wds/theming", () => {
addons.add("wds-addon/toolbar", {
id: "wds-addon/toolbar",
title: "Theming",
type: types.TOOL,
match: (args) => {
const { viewMode, storyId } = args;
// show the addon only on wds
if (
storyId &&
storyId?.includes("wds") &&
!!(viewMode && viewMode.match(/^(story|docs)$/))
)
return true;
return false;
},
render: ({ active }) => {
const [globals, updateGlobals] = useGlobals();
const updateGlobal = (key, value) => {
updateGlobals({
[key]: value,
});
};
return (
<WithTooltip
trigger="click"
placement="bottom"
tooltipShown={active}
closeOnClick
tooltip={
<Wrapper>
<div>
<H6>Border Radius</H6>
<StyledSelect
id="border-radius"
label="Border Radius"
size="100%"
defaultValue={globals.borderRadius}
onChange={(e) => updateGlobal("borderRadius", e.target.value)}
>
<option value="0px">Sharp</option>
<option value="0.375rem">Rounded</option>
<option value="1rem">Pill</option>
</StyledSelect>
</div>
<div>
<H6>Accent Color</H6>
<ColorControl
id="accent-color"
name="accent-color"
label="Accent Color"
defaultValue={globals.accentColor}
value={globals.accentColor}
onChange={(value) => updateGlobal("accentColor", value)}
/>
</div>
<div>
<H6>Font Family</H6>
<StyledSelect
id="font-family"
label="Font Family"
size="100%"
defaultValue={globals.fontFamily}
onChange={(e) => updateGlobal("fontFamily", e.target.value)}
>
<option value="">System Default</option>
{Object.keys(fontMetricsMap)
.filter((item) => {
return (
[
"-apple-system",
"BlinkMacSystemFont",
"Segoe UI",
].includes(item) === false
);
})
.map((font) => (
<option value={font} key={`font-famiy-${font}`}>
{font}
</option>
))}
</StyledSelect>
</div>
</Wrapper>
}
>
<IconButton key="wds-addon/toolbar" active={active} title="Theming">
<Icons icon="paintbrush" />
</IconButton>
</WithTooltip>
);
},
});
});

View File

@ -0,0 +1,25 @@
import { create } from "@storybook/theming";
export default create({
// UI
appBg: "#FFFFFF",
appContentBg: "#FFFFFF",
appBorderColor: "#E3E8EF",
appBorderRadius: 0,
// Toolbar default and active colors
barSelectedColor: "#191919",
// Form colors
inputBg: "#FFFFFF",
inputBorder: "#E3E8EF",
inputTextColor: "#364252",
inputBorderRadius: 0,
base: "light",
brandTitle: "Appsmith Design System",
brandUrl: "https://www.appsmith.com/",
brandImage:
"https://global-uploads.webflow.com/61531b23c347e4fbd4a84209/61531b23c347e41e24a8423e_Logo.svg",
brandTarget: "_blank",
});

View File

@ -0,0 +1,18 @@
import React from "react";
import { Resizable } from "re-resizable";
export const resizor = (Story, args) => {
const { parameters } = args;
return (
<Resizable
grid={[8, 8]}
defaultSize={{
width: parameters.width,
height: parameters.height,
}}
>
<Story />
</Resizable>
);
};

View File

@ -0,0 +1,46 @@
import React, { useEffect } from "react";
import webfontloader from "webfontloader";
import styled, { createGlobalStyle } from "styled-components";
import { createTokens, createGlobalFontStack } from "@design-system/wds";
const StyledContainer = styled.div`
${createTokens}
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
`;
const { fontFaces } = createGlobalFontStack();
const GlobalStyles = createGlobalStyle`
${fontFaces}
`;
export const theming = (Story, args) => {
// Load the font if it's not the default
useEffect(() => {
if (
args.globals.fontFamily &&
args.globals.fontFamily !== "System Default"
) {
webfontloader.load({
google: {
families: [`${args.globals.fontFamily}:300,400,500,700`],
},
});
}
}, [args.globals.fontFamily]);
return (
<StyledContainer
accentColor={args.globals.accentColor || "#553DE9"}
borderRadius={args.globals.borderRadius}
>
<GlobalStyles />
<Story fontFamily={args.globals.fontFamily} />
</StyledContainer>
);
};

View File

@ -0,0 +1,41 @@
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
async function webpackConfig(config) {
config.module.rules.push({
test: /\.(js|jsx|ts|tsx)$/,
use: {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
},
},
});
config.resolve.plugins.push(new TsconfigPathsPlugin());
return config;
}
module.exports = {
stories: [
"../../wds/src/**/*.stories.mdx",
"../../wds/src/**/*.stories.@(js|jsx|ts|tsx)",
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/preset-create-react-app",
"storybook-addon-pseudo-states",
"./addons/theming/register.js",
],
framework: "@storybook/react",
webpackFinal: webpackConfig,
core: {
builder: "@storybook/builder-webpack5",
},
};

View File

@ -0,0 +1,74 @@
<style>
div[role="main"] {
border: 1px solid #E3E8EF !important;
}
div[role="main"] > div {
box-shadow: none !important;
}
img[alt="Appsmith Design System"] {
width: 134px;
}
.sidebar-item[data-selected="true"] {
background-color: #E3E8EF !important;
color: #364252 !important;
font-weight: 500 !important;
}
.sidebar-item[data-selected="true"] > svg{
color: #364252 !important;
}
.sto-rfiy1p, .sto-f4xokn {
border-top: 2px solid transparent !important;
border-bottom: 2px solid transparent !important;
font-weight: 500 !important;
}
.tabbutton:focus {
border-bottom-color: #F86A2B !important;
}
.sto-f4xokn {
border-bottom-color: #F86A2B !important;
}
#storybook-explorer-tree > div {
margin: unset !important;
}
.sidebar-item {
padding-top: 6px !important;
padding-bottom: 6px !important;
}
#storybook-explorer-searchfield, button[title="Shortcuts"] {
border-radius: 0 !important;
border: 1px solid #E3E8EF !important;
box-shadow: none !important;
}
button[title="Shortcuts"]:focus:before {
border-radius: 0 !important;
border: 1px solid #E3E8EF !important;
}
.sto-1jd7xzf:focus,
.sto-qbvvbz input:focus,
.sto-dtdhmd:focus {
box-shadow: #F86A2B 0 0 0 1px inset !important;
}
.sto-t2643i:hover,
.sto-t2643i:focus-visible,
.sto-1k5e3f:hover,
.sto-1k5e3f:focus-visible,
.sto-14xfrgt:hover,
.sto-14xfrgt:focus-visible {
color: #F86A2B !important;
background: #f86a2b1f !important;
}
</style>

View File

@ -0,0 +1,10 @@
import { addons } from "@storybook/addons";
import appsmithTheme from "./appsmith-theme";
addons.setConfig({
theme: appsmithTheme,
selectedPanel: "ds-test",
sidebar: {
showRoots: false,
},
});

View File

@ -0,0 +1,17 @@
import { resizor } from "./decorators/resizor";
import { theming } from "./decorators/theming";
import "./styles.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
layout: "centered",
};
export const decorators = [resizor, theming];

View File

@ -0,0 +1,18 @@
@import url("../../wds/src/styles/tokens/raw.css");
@import url("../../wds/src/styles/tokens/semantic.css");
@import url("../../wds/src/styles/globals.css");
html, body, #root {
height: 100%;
width: 100%;
}
*, :after, :before {
border: 0 solid #e4e4e7;
box-sizing: border-box;
}
.innerZoomElementWrapper > * {
overflow:hidden;
}

View File

@ -0,0 +1,46 @@
{
"name": "@design-system/storybook",
"version": "1.0.0",
"main": "src/index.ts",
"author": "Valera Melnikov <valera@appsmith.com>, Pawan Kumar <pawan@appsmith.com>",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.20.12",
"@storybook/addon-actions": "^6.5.16",
"@storybook/addon-docs": "^6.5.16",
"@storybook/addon-essentials": "^6.5.16",
"@storybook/addon-interactions": "^6.5.16",
"@storybook/addon-links": "^6.5.16",
"@storybook/addon-postcss": "^2.0.0",
"@storybook/builder-webpack4": "^6.5.16",
"@storybook/builder-webpack5": "^6.5.16",
"@storybook/manager-webpack4": "^6.5.16",
"@storybook/manager-webpack5": "^6.5.16",
"@storybook/node-logger": "^6.5.16",
"@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.16",
"@storybook/testing-library": "^0.0.13",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
"autoprefixer": "^9.0.0",
"babel-loader": "^8.3.0",
"eslint-plugin-storybook": "^0.6.10",
"postcss": "^8",
"re-resizable": "^6.9.9",
"storybook-addon-pseudo-states": "^1.15.2",
"storybook-color-picker": "^3.1.0",
"tsconfig-paths-webpack-plugin": "^3.5.2"
},
"dependencies": {
"@capsizecss/core": "^3.1.0",
"@capsizecss/metrics": "^1.0.1",
"@design-system/wds": "*",
"colorjs.io": "^0.4.3",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"scripts": {
"storybook": "start-storybook -p 6006",
"build": "build-storybook"
}
}

View File

@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json",
}

View File

@ -0,0 +1,18 @@
{
"name": "@design-system/wds",
"version": "1.0.0",
"main": "src/index.ts",
"author": "Valera Melnikov <valera@appsmith.com>, Pawan Kumar <pawan@appsmith.com>",
"license": "MIT",
"devDependencies": {
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2"
},
"dependencies": {
"@capsizecss/core": "^3.1.0",
"@capsizecss/metrics": "^1.0.1",
"colorjs.io": "^0.4.3",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}

View File

@ -0,0 +1,84 @@
<!-- Button.stories.mdx -->
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Button } from './';
<Meta title="WDS/Button" component={Button} parameters={{
height: 32,
width: 180,
}} argTypes={{
children: {
name: 'children',
description: 'Text shown by button',
defaultValue: 'Button',
control: {
type: 'text'
},
table: {
type: {
summary: 'The label contents',
detail: 'Text displayed by the Badge'
}
}
}
}} />
export const Template = (args, { globals: { fontFamily } }) => <Button {...args} fontFamily={fontFamily} />;
# Button
A button is a clickable element that is used to trigger an action.
<Canvas>
<Story name="Default">
{Template.bind({})}
</Story>
</Canvas>
# Variants
There are 4 variants of the button component.
<Canvas>
<Story name="Filled Variant" args={{
children: 'Filled'
}}>
{Template.bind({})}
</Story>
<Story name="Outline Variant" args={{
variant: "outline",
children: 'Outline'
}}>
{Template.bind({})}
</Story>
<Story name="Light Variant" args={{
variant: "light",
children: 'Light'
}}>
{Template.bind({})}
</Story>
<Story name="Subtle Variant" args={{
variant: "subtle",
children: 'Subtle'
}}>
{Template.bind({})}
</Story>
</Canvas>
# States
<Canvas>
<Story name="Disabled State" args={{
isDisabled: true,
children: 'Disabled'
}}>
{Template.bind({})}
</Story>
<Story name="Loading State" args={{
isLoading: true,
children: 'Loading'
}}>
{Template.bind({})}
</Story>
</Canvas>

View File

@ -0,0 +1,81 @@
import React, { HTMLAttributes, useMemo, forwardRef } from "react";
import { Text } from "../Text";
import { Spinner } from "../Spinner";
import { StyledButton } from "./index.styled";
import { fontFamilyTypes } from "../../utils/typography";
// types
export enum ButtonVariant {
FILLED = "filled",
OUTLINE = "outline",
LIGHT = "light",
SUBTLE = "subtle",
}
export type ButtonProps = {
accentColor?: string;
variant?: ButtonVariant;
boxShadow?: string;
borderRadius?: string;
tooltip?: string;
children?: React.ReactNode;
isDisabled?: boolean;
isLoading?: boolean;
className?: string;
leadingIcon?: React.ReactNode;
trailingIcon?: React.ReactNode;
as?: keyof JSX.IntrinsicElements;
fontFamily?: fontFamilyTypes;
} & HTMLAttributes<HTMLButtonElement>;
// component
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const {
children,
fontFamily,
isDisabled,
isLoading,
leadingIcon,
trailingIcon,
variant = ButtonVariant.FILLED,
...rest
} = props;
const content = useMemo(() => {
if (isLoading) return <Spinner />;
return (
<>
{leadingIcon && <span data-component="leadingIcon">{leadingIcon}</span>}
{children && (
<Text data-component="text" fontFamily={fontFamily}>
{children}
</Text>
)}
{trailingIcon && (
<span data-component="trailingIcon">{trailingIcon}</span>
)}
</>
);
}, [isLoading, children, trailingIcon, leadingIcon, fontFamily]);
return (
<StyledButton
{...rest}
data-button
data-disabled={isDisabled || undefined}
data-loading={isLoading || undefined}
data-variant={variant}
disabled={isDisabled || undefined}
ref={ref}
variant={variant}
>
{content}
</StyledButton>
);
}) as typeof StyledButton;
Button.displayName = "Button";
export { Button };

View File

@ -0,0 +1,197 @@
import styled, { css } from "styled-components";
import {
lightenColor,
getComplementaryGrayscaleColor,
calulateHoverColor,
darkenColor,
parseColor,
} from "../../utils/colors";
import { ButtonProps } from "./Button";
/**
* creates locally scoped css variables to be used in variants styles
*
*/
export const variantTokens = css`
${({ accentColor: color }: Pick<ButtonProps, "accentColor" | "variant">) => {
if (!color) return "";
const accentColor = parseColor(color).toString({ format: "hex" });
const accentHoverColor = calulateHoverColor(color);
const lightAccentColor = lightenColor(color);
const accentActiveColor = darkenColor(accentHoverColor);
const lightAccentHoverColor = calulateHoverColor(lightAccentColor);
const textColor = getComplementaryGrayscaleColor(accentColor);
const onAccentBorderColor = darkenColor(color, 0.1);
const onAccentLightBorderColor = lightenColor(color, 0.98);
const lightAcctentActiveColor = darkenColor(lightAccentHoverColor, 0.03);
return css`
--wds-v2-color-bg-accent: ${accentColor};
--wds-v2-color-bg-accent-hover: ${accentHoverColor};
--wds-v2-color-bg-accent-light: ${lightAccentColor};
--wds-v2-color-bg-accent-active: ${accentActiveColor};
--wds-v2-color-bg-accent-light-active: ${lightAcctentActiveColor};
--wds-v2-color-bg-accent-light-hover: ${lightAccentHoverColor};
--wds-v2-color-text-accent: ${accentColor};
--wds-v2-color-text-onaccent: ${textColor};
--wds-v2-color-border-accent: ${accentColor};
--wds-vs-color-border-onaccent: ${onAccentBorderColor};
--wds-vs-color-border-onaccent-light: ${onAccentLightBorderColor};
`;
}}
`;
export const StyledButton = styled.button<ButtonProps>`
display: flex;
overflow: hidden;
text-align: center;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
outline: 0;
cursor: pointer;
white-space: nowrap;
gap: var(--wds-v2-spacing-4);
padding: var(--wds-v2-spacing-2) var(--wds-v2-spacing-4);
min-height: 32px;
border-radius: var(--wds-v2-radii);
box-shadow: var(--wds-v2-shadow);
border-width: 0;
${({ borderRadius }) => borderRadius && `--wds-v2-radii: ${borderRadius};`};
${({ boxShadow }) => boxShadow && `--wds-v2-shadow: ${boxShadow};`};
&[data-loading] {
pointer-events: none;
}
& [data-component="text"] {
width: 100%;
span {
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
${variantTokens}
/**
* ----------------------------------------------------------------------------
* variants
* ----------------------------------------------------------------------------
*/
&[data-variant="filled"] {
background-color: var(--wds-v2-color-bg-accent);
color: var(--wds-v2-color-text-onaccent);
border-color: transparent;
&:hover {
background-color: var(--wds-v2-color-bg-accent-hover);
}
&:active {
background-color: var(--wds-v2-color-bg-accent-active);
}
}
&[data-variant="outline"] {
border-width: 1px;
background-color: transparent;
color: var(--wds-v2-color-text-accent);
border-color: var(--wds-v2-color-border-accent);
&:hover {
background-color: var(--wds-v2-color-bg-accent-light-hover);
}
&:active {
background-color: var(--wds-v2-color-bg-accent-light-active);
}
&:hover:disabled {
background-color: transparent;
}
}
&[data-variant="light"] {
background: var(--wds-v2-color-bg-accent-light);
border-color: transparent;
color: var(--wds-v2-color-text-accent);
border-width: 0;
&:hover {
background: var(--wds-v2-color-bg-accent-light-hover);
}
&:active {
background: var(--wds-v2-color-bg-accent-light-active);
}
}
&[data-variant="subtle"] {
border-color: transparent;
color: var(--wds-v2-color-text-accent);
border-width: 0;
background: transparent;
&:hover {
background: var(--wds-v2-color-bg-accent-light-hover);
}
&:active {
background: var(--wds-v2-color-bg-accent-light-active);
}
}
&[data-variant="input"] {
text-align: left;
background: var(--wds-v2-color-bg-select);
box-shadow: rgb(27 31 36 / 4%) 0px 1px 0px,
rgb(255 255 255 / 25%) 0px 1px 0px inset;
border-width: 1px;
border-color: var(--wds-v2-color-border);
padding: var(--wds-v2-spacing-2) var(--wds-v2-spacing-2);
&:hover {
background-color: var(--wds-v2-color-bg-select-hover);
}
}
/**
* ----------------------------------------------------------------------------
* psudo states
* ----------------------------------------------------------------------------
*/
/* // we don't use :focus-visible because not all browsers (safari) have it yet */
&:focus {
outline: none;
box-shadow: 0 0 0 2px white, 0 0 0 4px var(--wds-v2-color-border-focus);
}
&:focus:not(:focus-visible) {
outline: none;
box-shadow: none;
}
&:hover {
text-decoration: none;
}
&:is([data-disabled]),
&:is(:disabled) {
pointer-events: none;
background: var(--wds-v2-color-bg-disabled);
color: var(--wds-v2-color-text-disabled);
box-shadow: none;
background-image: none;
border-color: transparent;
}
`;

View File

@ -0,0 +1,2 @@
export { Button } from "./Button";
export { transformV1ButtonProps } from "./utils";

View File

@ -0,0 +1,27 @@
import { ButtonVariant } from "./Button";
export const transformV1ButtonProps = (v1Props: any) => {
const { buttonColor, buttonVariant, text } = v1Props;
const transformedProps: any = {};
switch (buttonVariant) {
case "PRIMARY":
transformedProps.variant = ButtonVariant.FILLED;
break;
case "SECONDARY":
transformedProps.variant = ButtonVariant.OUTLINE;
break;
case "TERTIARY":
transformedProps.variant = ButtonVariant.SUBTLE;
break;
}
transformedProps.children = text;
transformedProps.accentColor = buttonColor;
return {
...v1Props,
...transformedProps,
};
};

View File

@ -0,0 +1,40 @@
import React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { ButtonGroup } from "./index";
import { Button } from "../Button";
export default {
title: "WDS/Button Group",
component: ButtonGroup,
argTypes: {
variant: {
defaultValue: "filled",
options: ["filled", "outline", "subtle", "light"],
control: { type: "radio" },
},
},
} as ComponentMeta<typeof Button>;
// eslint-disable-next-line react/function-component-definition
const Template: ComponentStory<typeof Button> = ({ orientation, ...args }) => {
return (
<ButtonGroup orientation={orientation}>
<Button {...args}>Option 1</Button>
<Button {...args}>Option 2</Button>
<Button {...args}>Option 3</Button>
</ButtonGroup>
);
};
export const TextStory = Template.bind({});
TextStory.storyName = "Button Group";
TextStory.args = {
isLoading: false,
isDisabled: false,
};
TextStory.parameters = {
height: "32px",
width: "300px",
};

View File

@ -0,0 +1,25 @@
import React, { forwardRef } from "react";
import { StyledContainer } from "./index.styled";
// types
export enum Orientation {
VERTICAL = "vertical",
HORIZONTAL = "horizontal",
}
export interface ButtonGroupProps
extends React.ComponentPropsWithoutRef<"div"> {
children?: React.ReactNode;
orientation?: Orientation;
}
// component
export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
(props, ref) => {
const { orientation = Orientation.HORIZONTAL, ...others } = props;
return <StyledContainer orientation={orientation} ref={ref} {...others} />;
},
);
ButtonGroup.displayName = "ButtonGroup";

View File

@ -0,0 +1,65 @@
import styled from "styled-components";
import { ButtonGroupProps } from "./ButtonGroup";
export const StyledContainer = styled.div<ButtonGroupProps>`
--border-width: 1px;
display: flex;
height: 100%;
width: 100%;
flex-direction: ${({ orientation }) =>
orientation === "vertical" ? "column" : "row"};
& [data-button] {
// increasing z index to make sure the focused button is on top of the others
&:not([data-disabled]):focus {
z-index: 1;
}
&:is([data-variant="filled"]):not([data-disabled]) {
border-color: var(--wds-vs-color-border-onaccent);
}
&:is([data-variant="light"]):not([data-disabled]) {
border-color: var(--wds-vs-color-border-onaccent-light);
}
&:first-child {
border-bottom-right-radius: 0;
${({ orientation }) =>
orientation === "vertical"
? "border-bottom-left-radius: 0; border-bottom-width: calc(var(--border-width) / 2);"
: "border-top-right-radius: 0; border-right-width: calc(var(--border-width) / 2);"}
}
&:last-of-type {
border-top-left-radius: 0;
${({ orientation }) =>
orientation === "vertical"
? "border-top-right-radius: 0; border-top-width: calc(var(--border-width) / 2);"
: "border-bottom-left-radius: 0; border-left-width: calc(var(--border-width) / 2);"}
}
&:not(:first-child):not(:last-of-type) {
border-radius: 0;
${({ orientation }) =>
orientation === "vertical"
? "border-top-width: calc(var(--border-width) / 2); border-bottom-width: calc(var(--border-width) / 2);"
: "border-left-width: calc(var(--border-width) / 2); border-right-width: calc(var(--border-width) / 2);"}
}
& + [data-button] {
${({ orientation }) =>
orientation === "vertical"
? "margin-top: calc(var(--border-width) * -1);"
: "margin-left: calc(var(--border-width) * -1);"}
@media (min-resolution: 192dpi) {
${({ orientation }) =>
orientation === "vertical" ? "margin-top: 0px;" : "margin-left: 0px;"}
}
}
}
`;

View File

@ -0,0 +1 @@
export { ButtonGroup } from "./ButtonGroup";

View File

@ -0,0 +1,23 @@
import styled from "styled-components";
import LoaderIcon from "remixicon-react/Loader2FillIcon";
export const StyledSpinner = styled(LoaderIcon)`
animation: spin 1s linear infinite;
height: 1.2rem;
width: 1.2rem;
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
& path {
fill: currentColor;
}
`;

View File

@ -0,0 +1,15 @@
import React from "react";
import { StyledSpinner } from "./index.styled";
type IconProps = {
className?: string;
};
const Spinner = (props: IconProps) => {
return <StyledSpinner {...props} />;
};
Spinner.displayName = "Spinner";
export { Spinner };

View File

@ -0,0 +1,31 @@
import React from "react";
import { StyledText } from "./index.styled";
import { fontFamilyTypes } from "../../utils/typography";
export type TextProps = {
children: React.ReactNode;
isLoading?: boolean;
isDisabled?: boolean;
fontFamily?: fontFamilyTypes;
fontSize?: string;
color?: string;
fontWeight?: "normal" | "bold" | "bolder" | "lighter";
textDecoration?: "none" | "underline" | "line-through";
fontStyle?: "normal" | "italic";
textAlign?: "left" | "center" | "right";
capHeight?: number;
lineGap?: number;
as?: keyof JSX.IntrinsicElements;
};
export const Text = React.forwardRef<HTMLParagraphElement, TextProps>(
(props, ref) => {
const { children, ...rest } = props;
return (
<StyledText ref={ref} {...rest}>
<span>{children}</span>
</StyledText>
);
},
) as typeof StyledText;

View File

@ -0,0 +1,36 @@
import styled, { css } from "styled-components";
import { TextProps } from "./Text";
import { createTypographyStyles } from "../../utils/typography";
const shouldForwardProp = (prop: any) => {
const propsToOmit = [
"fontWeight",
"fontStyle",
"color",
"textAlign",
"textDecoration",
];
return !propsToOmit.includes(prop);
};
const typographyStyles = css`
${(props: TextProps) => {
const { capHeight = 10, fontFamily, lineGap = 8 } = props;
const styles = createTypographyStyles({ fontFamily, lineGap, capHeight });
return styles;
}}
`;
export const StyledText = styled.p.withConfig({ shouldForwardProp })<TextProps>`
margin: 0;
color: ${({ color }) => color};
font-weight: ${({ fontWeight }) => fontWeight};
text-decoration: ${({ textDecoration }) => textDecoration};
font-style: ${({ fontStyle }) => fontStyle};
text-align: ${({ textAlign }) => textAlign};
${typographyStyles}
`;

View File

@ -0,0 +1 @@
export { Text } from "./Text";

View File

@ -0,0 +1,80 @@
<!-- index.stories.mdx -->
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="WDS/Introduction" />
# WDS Foundations & Principles
Foundational principles and guidelines are a common concept in design systems. They commonly include visual primitives (colors, typography, spacing), conventions, accessibility standards, essential interaction patterns and more depending on the needs and maturity of each individual system. WDS does not have them today. What follows are some starting foundations and principles for WDS.
Well be iterating on these as we go, putting rules into writing once we have an understanding of problem space and practical solutions and rules to address it.
## Design Principles
### Conventional
![Untitled](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/09a026d7-9771-490f-9406-76ae115f6cb5/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230223%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230223T100640Z&X-Amz-Expires=86400&X-Amz-Signature=d10513676542b155d9958bca76c9b667bd53ee9d36b23a0e52b647dc85f657c7&X-Amz-SignedHeaders=host&response-content-disposition=filename%3D%22Untitled.png%22&x-id=GetObject)
WDS should enable rapid creation of conventional UI, not experimental one. It should consist of elements that our builders (and their users) have familiarity with (they know how it works, they know how it looks, theyve seen this UI in other applications before).
### **Cohesive**
![Untitled](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/ec3e9dd4-b21f-4218-aa59-f17be255dadc/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230223%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230223T100708Z&X-Amz-Expires=86400&X-Amz-Signature=abdc058020ec1e8d1b5c1142b0dab03a4d7c7ea90dd155626b189d6908152604&X-Amz-SignedHeaders=host&response-content-disposition=filename%3D%22Untitled.png%22&x-id=GetObject)
Any element of WDS should seamlessly mix with other elements of the design system without breaking visual harmony.
### **Composable (Modular)**
![Untitled](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/a21a234b-67e4-4ea0-a761-081b8fb8bf02/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230223%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230223T100720Z&X-Amz-Expires=86400&X-Amz-Signature=0eece268d8589aec20c5bcb8941609a9fcbebf67ee298275b39aa97e7d097bf0&X-Amz-SignedHeaders=host&response-content-disposition=filename%3D%22Untitled.png%22&x-id=GetObject)
Elements of WDS can be organised hierarchically and composed to produce more sophisticated elements, which in turn can be be further composed.
### Context aware (**Responsive)**
![Untitled](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/c0733b19-957e-4784-80e4-a5a73509eb83/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230223%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230223T100731Z&X-Amz-Expires=86400&X-Amz-Signature=44a55a53bc79e08a66b87cc148e76879803515a848b6edb456c7f09398be44f0&X-Amz-SignedHeaders=host&response-content-disposition=filename%3D%22Untitled.png%22&x-id=GetObject)
Elements need to adapt to their context, look and work well on small and large screens.
### C**ustomisable (Themeable)**
![Untitled](hhttps://s3.us-west-2.amazonaws.com/secure.notion-static.com/10485f62-f73e-4f85-93c7-513c69b8e6cf/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230223%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230223T100745Z&X-Amz-Expires=86400&X-Amz-Signature=cd98f964fcfc73e72534fe150d250d615fd41b9341f9e7dd227feef6ce34cd39&X-Amz-SignedHeaders=host&response-content-disposition=filename%3D%22Untitled.png%22&x-id=GetObject)
Elements in the system needs to account for customisation of visual style. e.g. Our builders may want to change colors, fonts, spacing. These are overarching parameters, that will be adjusted on a theme level. Theme controls the looks of individual UI elements and makes sure they are consistent.
## Design Foundations
### **Color**
![Untitled](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/9a515a5c-3610-4266-88db-b3d50d057deb/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230223%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230223T100808Z&X-Amz-Expires=86400&X-Amz-Signature=bd830e4f37c4715b1bdfa35007a74a88983722e331212e271ebd24b7feff4e9d&X-Amz-SignedHeaders=host&response-content-disposition=filename%3D%22Untitled.png%22&x-id=GetObject)
Color has many applications in UI design: drawing users attention, setting tone and mood, color-coding information architecture etc. There are millions of colors that our displays can show. Tens and hundreds of them are often needed in the UI. Those need to be organised and made harmonious.
### **Typography**
![Untitled](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/0b068bac-2262-44ce-871a-736f4bb25645/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230223%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230223T100822Z&X-Amz-Expires=86400&X-Amz-Signature=8d12da76b8fde6c73c38a758ad3e752a53c4a908caf14d1e7db2a6ec62dd6b1e&X-Amz-SignedHeaders=host&response-content-disposition=filename%3D%22Untitled.png%22&x-id=GetObject)
Text is the most common base element of the UI. It is highly efficient and much easier understood than e.g. iconography. We want to ensure it is legible to take the use of our end UI as smooth as possible. This will be enabled by having a hierarchy, a system that controls various typographic properties.
### Layout
![Untitled](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/3c9ecd0f-de2a-49be-89bd-2263608764ce/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230223%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230223T100843Z&X-Amz-Expires=86400&X-Amz-Signature=e665efcf29c824a9dc7589d0e3f5ce9d965cf19a050e8b01019cdcfc9af71eac&X-Amz-SignedHeaders=host&response-content-disposition=filename%3D%22Untitled.png%22&x-id=GetObject)
Layout foundation defines spatial distribution of elements on the screen, defines amount of space between elements and groups, enables responsive behaviour and guides users through the UI adhering to gestalt principles.
### Iconography
![Untitled](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/348f1940-86c3-4137-aae3-57b0330d580d/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230223%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230223T100846Z&X-Amz-Expires=86400&X-Amz-Signature=ed83c9fbce2468596bb5388831831225622e9c5e938b4f9e9049706e926fb771&X-Amz-SignedHeaders=host&response-content-disposition=filename%3D%22Untitled.png%22&x-id=GetObject)
Defined and consistent iconography and imagery sets the mood and tone for the UI.
### Layering / Depth
![Untitled](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/dedff06f-fcbf-43f5-b735-5ce594b82900/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230223%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230223T100849Z&X-Amz-Expires=86400&X-Amz-Signature=44fcb776fe1d3a0a10881923d0d7bcbcbe411eed276ae51db02f48ff19ea3d62&X-Amz-SignedHeaders=host&response-content-disposition=filename%3D%22Untitled.png%22&x-id=GetObject)
UI elements can be stacked and layered on top of each other and organised into groups. WDS will define how these compositions behave and how they should be expressed visually (e.g. with color and shadow).
### Interactive states
![Untitled](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/2033bd94-e9ae-47f2-92b3-4a519c790d71/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230223%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230223T100851Z&X-Amz-Expires=86400&X-Amz-Signature=0fe0d5c33fe3a6ae8acb17d41f8a9cadbff2d85de8e5f1081d4ce26f46f93448&X-Amz-SignedHeaders=host&response-content-disposition=filename%3D%22Untitled.png%22&x-id=GetObject)
UI elements can have multiple interactive states. WDS will define how to deal with those in consistent and predictable fashion.

View File

@ -0,0 +1,3 @@
export { Button } from "./components/Button";
export { createTokens } from "./utils/createTokens";
export { fontMetricsMap, createGlobalFontStack } from "./utils/typography";

View File

@ -0,0 +1,64 @@
:root {
/**
* ----------------------------------------------------------------------------
* color pallette
*-----------------------------------------------------------------------------
*/
--wds-v2-color-transparent: "#00000000";
--wds-v2-color-white: #ffffff;
--wds-v2-color-gray-100: #e3e3e3;
--wds-v2-color-gray-200: #a9a7a7;
--wds-v2-color-gray-300: #858283;
--wds-v2-color-gray-400: #4b4849;
--wds-v2-color-gray-500: #202020;
--wds-v2-color-gray-600: #090707;
--wds-v2-color-green-100: #b0f7c2;
--wds-v2-color-green-200: #37c56e;
--wds-v2-color-green-300: #149c51;
--wds-v2-color-green-400: #00592a;
--wds-v2-color-green-500: #00280f;
--wds-v2-color-green-600: #000b03;
--wds-v2-color-red-100: #ffd9d2;
--wds-v2-color-red-200: #ff7a65;
--wds-v2-color-red-300: #e43e29;
--wds-v2-color-red-400: #891103;
--wds-v2-color-red-500: #39110b;
--wds-v2-color-red-600: #120402;
--wds-v2-color-yellow-100: #ffdbb8;
--wds-v2-color-yellow-200: #ed9012;
--wds-v2-color-yellow-300: #cc7b00;
--wds-v2-color-yellow-400: #733800;
--wds-v2-color-yellow-500: #381900;
--wds-v2-color-yellow-600: #1a0900;
--wds-v2-color-purple-100: #dde1ff;
--wds-v2-color-purple-200: #969cff;
--wds-v2-color-purple-300: #6f69ff;
--wds-v2-color-purple-400: #4000c7;
--wds-v2-color-purple-500: #1b0063;
--wds-v2-color-purple-600: #060026;
--wds-v2-color-blue-100: #cce2ff;
--wds-v2-color-blue-200: #68aaff;
--wds-v2-color-blue-300: #2a82ea;
--wds-v2-color-blue-400: #024790;
--wds-v2-color-blue-500: #001f46;
--wds-v2-color-blue-600: #000719;
/**
* ----------------------------------------------------------------------------
* spacing
*-----------------------------------------------------------------------------
*/
--wds-v2-spacing-root: 4px;
--wds-v2-spacing-0: 0px;
--wds-v2-spacing-1: var(--wds-v2-spacing-root);
--wds-v2-spacing-2: calc(var(--wds-v2-spacing-root) * 2);
--wds-v2-spacing-4: calc(var(--wds-v2-spacing-root) * 4);
}

View File

@ -0,0 +1,11 @@
:root {
--wds-v2-color-bg-disabled: var(--wds-v2-color-gray-100);
--wds-v2-color-text-disabled: var(--wds-v2-color-gray-400);
--wds-v2-color-border: var(--wds-v2-color-gray-200);
--wds-v2-color-border-disabled: var(--wds-v2-color-gray-200);
--wds-v2-color-border-focus: var(--wds-v2-color-blue-300);
--wds-v2-color-bg-select: var(--wds-v2-color-white);
--wds-v2-color-bg-select-hover: var(--wds-v2-color-gray-100);
}

View File

@ -0,0 +1,86 @@
import Color from "colorjs.io";
/**
* returns black or white based on the constrast of the color compare to white
* using APCA algorithm
*
* @param color
* @returns
*/
export const getComplementaryGrayscaleColor = (hex = "#000") => {
const bg = parseColor(hex);
const text = new Color("#fff");
const contrast = bg.contrast(text, "APCA");
// if contrast is less than -35 then the text color should be white
if (contrast < -35) return "#fff";
return "#000";
};
/**
* lightens color
*
* @param color
* @param amount
* @returns
*/
export const lightenColor = (hex = "#000", lightness = 0.9) => {
const color = parseColor(hex);
color.set("oklch.l", () => lightness);
return color.toString({ format: "hex" });
};
/**
* darkens color by a given amount
*
* @param hex
* @param lightness
* @returns
*/
export const darkenColor = (hex = "#000", lightness = 0.03) => {
const color = parseColor(hex);
color.set("oklch.l", (l: any) => l - lightness);
return color.toString({ format: "hex" });
};
/**
* calculate the hover color
*
* @param hex
* @returns
*/
export const calulateHoverColor = (hex = "#000") => {
const color = parseColor(hex);
switch (true) {
case color.get("oklch.l") > 0.35:
color.set("oklch.l", (l: any) => l + 0.03);
break;
case color.get("oklch.l") < 0.35:
color.set("oklch.l", (l: any) => l - 0.03);
break;
}
return color.toString({ format: "hex" });
};
/**
* Parses a color and returns a color object
* if the color is invalid it returns black
*
* @param hex
* @returns
*/
export const parseColor = (hex = "#000") => {
try {
return new Color(hex);
} catch (error) {
return new Color("#000");
}
};

View File

@ -0,0 +1,54 @@
import { css, CSSProperties } from "styled-components";
import {
lightenColor,
getComplementaryGrayscaleColor,
calulateHoverColor,
darkenColor,
parseColor,
} from "./colors";
/**
* This function is used to create tokens for widgets
*/
export const createTokens = css`
${({
accentColor: color,
borderRadius,
boxShadow,
}: {
accentColor: CSSProperties["color"];
borderRadius: CSSProperties["borderRadius"];
boxShadow: CSSProperties["boxShadow"];
}) => {
const accentColor = parseColor(color).toString({ format: "hex" });
const accentHoverColor = calulateHoverColor(color);
const lightAccentColor = lightenColor(color);
const accentActiveColor = darkenColor(accentHoverColor);
const lightAccentHoverColor = calulateHoverColor(lightAccentColor);
const complementaryAccentColor = getComplementaryGrayscaleColor(
accentColor,
);
const lightAcctentActiveColor = darkenColor(lightAccentHoverColor, 0.03);
const onAccentBorderColor = darkenColor(color, 0.1);
const onAccentLightBorderColor = lightenColor(color, 0.98);
return css`
--wds-v2-color-bg-accent: ${accentColor};
--wds-v2-color-bg-accent-hover: ${accentHoverColor};
--wds-v2-color-bg-accent-light: ${lightAccentColor};
--wds-v2-color-bg-accent-active: ${accentActiveColor};
--wds-v2-color-bg-accent-light-active: ${lightAcctentActiveColor};
--wds-v2-color-bg-accent-light-hover: ${lightAccentHoverColor};
--wds-v2-color-text-accent: ${accentColor};
--wds-v2-color-text-onaccent: ${complementaryAccentColor};
--wds-v2-color-border-accent: ${accentColor};
--wds-vs-color-border-onaccent: ${onAccentBorderColor};
--wds-vs-color-border-onaccent-light: ${onAccentLightBorderColor};
--wds-v2-shadow: ${boxShadow};
--wds-v2-radii: ${borderRadius};
`;
}}
`;

View File

@ -0,0 +1,79 @@
import { createStyleObject, createFontStack } from "@capsizecss/core";
import arial from "@capsizecss/metrics/arial";
import inter from "@capsizecss/metrics/inter";
import rubik from "@capsizecss/metrics/rubik";
import roboto from "@capsizecss/metrics/roboto";
import ubuntu from "@capsizecss/metrics/ubuntu";
import poppins from "@capsizecss/metrics/poppins";
import segoeUI from "@capsizecss/metrics/segoeUI";
import openSans from "@capsizecss/metrics/openSans";
import notoSans from "@capsizecss/metrics/notoSans";
import montserrat from "@capsizecss/metrics/montserrat";
import nunitoSans from "@capsizecss/metrics/nunitoSans";
import appleSystem from "@capsizecss/metrics/appleSystem";
import BlinkMacSystemFont from "@capsizecss/metrics/blinkMacSystemFont";
export const fontMetricsMap = {
Poppins: poppins,
Inter: inter,
Roboto: roboto,
Rubik: rubik,
Ubuntu: ubuntu,
"Noto Sans": notoSans,
"Open Sans": openSans,
Montserrat: montserrat,
"Nunito Sans": nunitoSans,
Arial: arial,
"-apple-system": appleSystem,
BlinkMacSystemFont: BlinkMacSystemFont,
"Segoe UI": segoeUI,
} as const;
export type fontFamilyTypes = keyof typeof fontMetricsMap;
type createTypographyStylesProps = {
fontFamily?: fontFamilyTypes;
capHeight: number;
lineGap: number;
};
export const createTypographyStyles = (props: createTypographyStylesProps) => {
const { capHeight, fontFamily, lineGap } = props;
// if there is no font family, use the default font stack
if (!fontFamily) {
const styles = createStyleObject({
capHeight,
lineGap,
fontMetrics: appleSystem,
});
return {
fontFamily: `-apple-system, BlinkMacSystemFont, "-apple-system Fallback: Segoe UI", "-apple-system Fallback: Roboto"`,
...styles,
};
}
const styles = createStyleObject({
capHeight,
lineGap,
fontMetrics: fontMetricsMap[fontFamily],
});
return {
fontFamily: `"${fontFamily}"`,
...styles,
};
};
/**
* create a global font stack
*
* @returns
*/
export const createGlobalFontStack = () => {
return createFontStack([appleSystem, BlinkMacSystemFont, segoeUI, roboto], {
fontFaceFormat: "styleString",
});
};

View File

@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}

View File

@ -57,7 +57,6 @@ import { ERROR_CODES } from "@appsmith/constants/ApiConstants";
import TemplatesListLoader from "pages/Templates/loader";
import { fetchFeatureFlagsInit } from "actions/userActions";
import FeatureFlags from "entities/FeatureFlags";
import WDSPage from "components/wds/Showcase";
import { getCurrentTenant } from "@appsmith/actions/tenantActions";
import { getDefaultAdminSettingsPath } from "@appsmith/utils/adminSettingsHelpers";
import { getCurrentUser as getCurrentUserSelector } from "selectors/usersSelectors";
@ -90,7 +89,6 @@ export function Routes() {
<SentryRoute component={WorkspaceLoader} path={WORKSPACE_URL} />
<SentryRoute component={Users} exact path={USERS_URL} />
<SentryRoute component={UserAuth} path={USER_AUTH_URL} />
<SentryRoute component={WDSPage} path="/wds" />
<SentryRoute
component={ApplicationListLoader}
exact

View File

@ -1,180 +0,0 @@
import React from "react";
import styled from "styled-components";
import {
IButtonProps,
MaybeElement,
Button as BlueprintButton,
} from "@blueprintjs/core";
import { IconName } from "@blueprintjs/icons";
import { withTooltip } from "components/wds";
import { Colors } from "constants/Colors";
import _ from "lodash";
import {
ButtonPlacement,
ButtonVariant,
ButtonVariantTypes,
} from "components/constants";
import {
getComplementaryGrayscaleColor,
lightenColor,
} from "widgets/WidgetUtils";
import { borderRadiusOptions } from "constants/ThemeConstants";
import withRecaptcha, { RecaptchaProps } from "./withRecaptcha";
type ButtonStyleProps = {
buttonColor?: string;
buttonVariant?: ButtonVariant;
iconName?: IconName;
placement?: ButtonPlacement;
justifyContent?:
| "flex-start"
| "flex-end"
| "center"
| "space-between"
| "space-around"
| "space-evenly";
};
export interface ButtonProps
extends IButtonProps,
ButtonStyleProps,
RecaptchaProps {
variant?: keyof typeof VariantTypes;
boxShadow?: string;
borderRadius?: string;
tooltip?: string;
children?: React.ReactNode;
leftIcon?: IconName | MaybeElement;
isDisabled?: boolean;
isLoading?: boolean;
}
enum VariantTypes {
solid = "solid",
outline = "outline",
ghost = "ghost",
link = "link",
}
export const StyledButton = styled((props) => (
<BlueprintButton
{..._.omit(props, [
"borderRadius",
"boxShadow",
"buttonColor",
"buttonVariant",
"variant",
"justifyContent",
])}
/>
))<ButtonProps>`
gap: 8px;
height: 100%;
outline: none;
padding: 0px 10px;
background-image: none !important;
border-radius: ${({ borderRadius }) => borderRadius};
box-shadow: ${({ boxShadow }) => `${boxShadow}`} !important;
justify-content: ${({ justifyContent }) => `${justifyContent}`} !important;
flex-direction: ${({ iconAlign }) => `${iconAlign}`};
${({ buttonColor }) => `
&.button--solid {
&:enabled {
background: ${buttonColor};
color: ${getComplementaryGrayscaleColor(buttonColor)}
}
}
&.button--outline {
&:enabled {
background: none;
border: 1px solid ${buttonColor};
color: ${buttonColor};
}
&:enabled:hover {
background: ${lightenColor(buttonColor)};
}
}
&.button--ghost {
&:enabled {
background: none;
color: ${buttonColor};
}
&:enabled:hover {
background: ${lightenColor(buttonColor)};
}
}
&.button--link {
&:enabled {
background: none;
color: ${buttonColor};
}
&:enabled:hover {
text-decoration: underline;
}
}
&:disabled {
background-color: ${Colors.GREY_1} !important;
color: ${Colors.GREY_9} !important;
box-shadow: none !important;
pointer-events: none;
border-color: ${Colors.GREY_1} !important;
> span {
color: ${Colors.GREY_9} !important;
}
}
& > * {
margin-right: 0;
}
& > span, & > span.bp3-icon {
max-height: 100%;
max-width: 99%;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
line-height: normal;
color: inherit;
}
`}
`;
function Button(props: ButtonProps) {
const { children, isDisabled, isLoading, leftIcon, ...rest } = props;
return (
<StyledButton
{...rest}
className={`button--${props.variant} ${props.className}`}
disabled={isDisabled}
icon={leftIcon}
loading={isLoading}
text={children}
/>
);
}
Button.defaultProps = {
buttonVariant: ButtonVariantTypes.PRIMARY,
disabled: false,
text: "Button Text",
minimal: true,
variant: "solid",
buttonColor: "#553DE9",
borderRadius: borderRadiusOptions.md,
justifyContent: "center",
} as ButtonProps;
export default withRecaptcha(withTooltip(Button));

View File

@ -1,172 +0,0 @@
import React, { useRef, useState } from "react";
import styled from "styled-components";
import { useScript, ScriptStatus, AddScriptTo } from "utils/hooks/useScript";
import {
GOOGLE_RECAPTCHA_KEY_ERROR,
GOOGLE_RECAPTCHA_DOMAIN_ERROR,
createMessage,
} from "@appsmith/constants/messages";
import { RecaptchaType, RecaptchaTypes } from "components/constants";
import ReCAPTCHA from "react-google-recaptcha";
import { Variant } from "design-system-old";
const RecaptchaWrapper = styled.div`
position: relative;
.grecaptcha-badge {
visibility: hidden;
}
`;
export interface RecaptchaProps {
googleRecaptchaKey?: string;
clickWithRecaptcha?: (token: string) => void;
handleRecaptchaV2Loading?: (isLoading: boolean) => void;
recaptchaType?: RecaptchaType;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
}
import { Toaster } from "design-system-old";
export default function withRecaptcha<
T extends RecaptchaProps = RecaptchaProps
>(WrappedComponent: React.ComponentType<T>) {
const displayName =
WrappedComponent.displayName || WrappedComponent.name || "Component";
function ComponentWithRecaptcha(props: T) {
if (!props.googleRecaptchaKey) {
return <WrappedComponent {...props} {...(props as T)} />;
}
const handleError = (
event: React.MouseEvent<HTMLElement>,
error: string,
) => {
Toaster.show({
text: error,
variant: Variant.danger,
});
props.onClick && props.onClick(event);
};
if (props.recaptchaType === RecaptchaTypes.V2) {
return (
<RecaptchaV2Component {...props} handleError={handleError}>
<WrappedComponent {...props} {...(props as T)} />
</RecaptchaV2Component>
);
} else {
return (
<RecaptchaV3Component {...props} handleError={handleError}>
<WrappedComponent {...props} {...(props as T)} />
</RecaptchaV3Component>
);
}
}
ComponentWithRecaptcha.displayName = `withRecaptcha(${displayName})`;
return ComponentWithRecaptcha;
}
function RecaptchaV2Component(
props: {
children: any;
recaptchaType?: RecaptchaType;
handleError: (event: React.MouseEvent<HTMLElement>, error: string) => void;
} & RecaptchaProps,
) {
const recaptchaRef = useRef<ReCAPTCHA>(null);
const [isInvalidKey, setInvalidKey] = useState(false);
const handleRecaptchaLoading = (isloading: boolean) => {
props.handleRecaptchaV2Loading && props.handleRecaptchaV2Loading(isloading);
};
const handleBtnClick = async (event: React.MouseEvent<HTMLElement>) => {
if (isInvalidKey) {
// Handle incorrent google recaptcha site key
props.handleError(event, createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
} else {
handleRecaptchaLoading(true);
try {
await recaptchaRef?.current?.reset();
const token = await recaptchaRef?.current?.executeAsync();
if (token && typeof props.clickWithRecaptcha === "function") {
props.clickWithRecaptcha(token);
} else {
// Handle incorrent google recaptcha site key
props.handleError(event, createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
}
handleRecaptchaLoading(false);
} catch (err) {
handleRecaptchaLoading(false);
// Handle error due to google recaptcha key of different domain
props.handleError(event, createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR));
}
}
};
return (
<RecaptchaWrapper onClick={handleBtnClick}>
{props.children}
<ReCAPTCHA
onErrored={() => setInvalidKey(true)}
ref={recaptchaRef}
sitekey={props.googleRecaptchaKey || ""}
size="invisible"
/>
</RecaptchaWrapper>
);
}
function RecaptchaV3Component(
props: {
children: any;
recaptchaType?: RecaptchaType;
handleError: (event: React.MouseEvent<HTMLElement>, error: string) => void;
} & RecaptchaProps,
) {
// Check if a string is a valid JSON string
const checkValidJson = (inputString: string): boolean => {
return !inputString.includes('"');
};
const handleBtnClick = (event: React.MouseEvent<HTMLElement>) => {
if (status === ScriptStatus.READY) {
(window as any).grecaptcha.ready(() => {
try {
(window as any).grecaptcha
.execute(props.googleRecaptchaKey, {
action: "submit",
})
.then((token: any) => {
if (typeof props.clickWithRecaptcha === "function") {
props.clickWithRecaptcha(token);
}
})
.catch(() => {
// Handle incorrent google recaptcha site key
props.handleError(
event,
createMessage(GOOGLE_RECAPTCHA_KEY_ERROR),
);
});
} catch (err) {
// Handle error due to google recaptcha key of different domain
props.handleError(
event,
createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR),
);
}
});
}
};
let validGoogleRecaptchaKey = props.googleRecaptchaKey;
if (validGoogleRecaptchaKey && !checkValidJson(validGoogleRecaptchaKey)) {
validGoogleRecaptchaKey = undefined;
}
const status = useScript(
`https://www.google.com/recaptcha/api.js?render=${validGoogleRecaptchaKey}`,
AddScriptTo.HEAD,
);
return <div onClick={handleBtnClick}>{props.children}</div>;
}

View File

@ -1,53 +0,0 @@
import React from "react";
import {
Popover,
Menu as BMenu,
MenuItem as BMenuItem,
IMenuProps,
IMenuItemProps,
} from "@blueprintjs/core";
type Props = {
children: React.ReactElement[] | React.ReactElement;
};
function Menu(props: Props) {
const menus =
(Array.isArray(props.children) &&
props.children.find(
(child: any) => child.type.displayName === "MenuList",
)) ||
undefined;
const trigger =
Array.isArray(props.children) &&
props.children.find(
(child: any) => child.type.displayName === "MenuTrigger",
);
return (
<Popover content={menus} popoverClassName="Menu-v2" transitionDuration={-1}>
{trigger}
</Popover>
);
}
function MenuList(props: IMenuProps) {
return <BMenu {...props} />;
}
MenuList.displayName = "MenuList";
function MenuTrigger(props: any) {
return <div {...props} />;
}
MenuTrigger.displayName = "MenuTrigger";
function MenuItem(props: IMenuItemProps) {
return <BMenuItem {...props} />;
}
MenuItem.displayName = "MenuItem";
export { Menu, MenuList, MenuItem, MenuTrigger };

View File

@ -1,57 +0,0 @@
import React from "react";
import {
Popover,
Menu,
MenuItem,
IMenuProps,
IMenuItemProps,
} from "@blueprintjs/core";
type Props = {
children: React.ReactElement[] | React.ReactElement;
};
function Select(props: Props) {
const menus =
(Array.isArray(props.children) &&
props.children.find(
(child: any) => child.type.displayName === "SelectList",
)) ||
undefined;
const trigger =
Array.isArray(props.children) &&
props.children.find(
(child: any) => child.type.displayName === "SelectTrigger",
);
return (
<Popover
content={menus}
popoverClassName="Select-v2"
transitionDuration={-1}
>
{trigger}
</Popover>
);
}
function SelectList(props: IMenuProps) {
return <Menu {...props} />;
}
SelectList.displayName = "SelectList";
function SelectTrigger(props: any) {
return <div {...props} />;
}
SelectTrigger.displayName = "SelectTrigger";
function SelectOption(props: IMenuItemProps) {
return <MenuItem {...props} />;
}
SelectOption.displayName = "SelectOption";
export { Select, SelectList, SelectOption, SelectTrigger };

View File

@ -1,149 +0,0 @@
import React, { useState } from "react";
import { Checkbox, Button } from "components/wds";
import { borderRadiusOptions } from "constants/ThemeConstants";
function Showcase() {
const [borderRadius, setBorderRadius] = useState("0px");
const theme = {
borderRadius,
};
return (
<div className="container min-h-screen pt-12 mx-auto">
<h1 className="mt-12 space-y-8 text-3xl font-bold">
Widgets Design System
</h1>
<h1>Theme Options</h1>
<div>Border radius</div>
<div className="flex gap-2">
{Object.keys(borderRadiusOptions).map((optionKey) => (
<button
className={`flex items-center justify-center w-8 h-8 bg-trueGray-100 ring-gray-700 cursor-pointer hover:bg-trueGray-50 ${
borderRadius === borderRadiusOptions[optionKey] ? "ring-1" : ""
}`}
key={optionKey}
onClick={() => {
setBorderRadius(borderRadiusOptions[optionKey]);
}}
>
<div
className="w-4 h-4 border-t-2 border-l-2 border-gray-600 rounded-"
style={{ borderTopLeftRadius: borderRadiusOptions[optionKey] }}
/>
</button>
))}
</div>
<div className="space-y-5">
<div className="mt-5">
<h2 className="my-2 text-xl font-semibold">Checkbox</h2>
<div className="space-y-3">
<div className="space-y-1">
<h3 className="text-gray-500">States</h3>
<div className="flex space-x-3">
<Checkbox checked label="Active" {...theme} />
<Checkbox
checked={false}
disabled
label="Disabled"
{...theme}
/>
<Checkbox checked={false} hasError label="Error" {...theme} />
<Checkbox
checked={false}
indeterminate
label="Indeterminate"
{...theme}
/>
</div>
</div>
</div>
</div>
{/* checkbox end */}
{/* buttons */}
<div className="">
<h2 className="my-2 text-xl font-semibold">Buttons</h2>
<div className="space-y-3">
<div className="space-y-1">
<h3 className="text-gray-500">Types</h3>
<div className="flex space-x-3">
<Button leftIcon="download" {...theme} />
<Button variant="solid" {...theme}>
Solid
</Button>
<Button variant="outline" {...theme}>
Outline
</Button>
<Button variant="ghost" {...theme}>
Ghost
</Button>
<Button variant="link" {...theme}>
Link
</Button>
</div>
</div>
<div className="space-y-1">
<h3 className="text-gray-500">States</h3>
<div className="flex space-x-3">
<Button {...theme}>Default</Button>
<Button isDisabled {...theme}>
Disalbed
</Button>
<Button isLoading {...theme}>
Loading
</Button>
</div>
</div>
<div className="space-y-1">
<h3 className="text-gray-500">Icon and Alignment</h3>
<div className="flex space-x-3">
<Button className="w-40" leftIcon="download" {...theme}>
With Icon
</Button>
<Button
className="w-40"
justifyContent="space-between"
leftIcon="download"
{...theme}
>
With Icon
</Button>
<Button
className="w-40"
justifyContent="flex-start"
leftIcon="download"
{...theme}
>
With Icon
</Button>
<Button
className="w-40"
justifyContent="flex-end"
leftIcon="download"
{...theme}
>
With Icon
</Button>
</div>
</div>
<div className="space-y-1">
<h3 className="text-gray-500">Misc</h3>
<div className="flex space-x-3">
<Button tooltip="This is tooltip content" {...theme}>
Tooltip
</Button>
</div>
</div>
</div>
</div>
{/*button end */}
</div>
</div>
);
}
export default Showcase;

View File

@ -1 +0,0 @@
export { default as withTooltip } from "./withTooltip";

View File

@ -1,49 +0,0 @@
import React from "react";
import styled from "styled-components";
import { TooltipComponent as Tooltip } from "design-system-old";
const ToolTipWrapper = styled.div`
height: 100%;
&& .bp3-popover2-target {
height: 100%;
width: 100%;
& > div {
height: 100%;
}
}
`;
interface TooltipProps {
tooltip?: string;
isDisabled?: boolean;
}
export default function withTooltip<T extends TooltipProps = TooltipProps>(
WrappedComponent: React.ComponentType<T>,
) {
const displayName =
WrappedComponent.displayName || WrappedComponent.name || "Component";
function ComponentWithTooltip(props: T) {
if (props.tooltip) {
return (
<ToolTipWrapper>
<Tooltip
content={props.tooltip}
disabled={props.isDisabled}
hoverOpenDelay={200}
position="top"
>
<WrappedComponent {...props} {...(props as T)} />
</Tooltip>
</ToolTipWrapper>
);
}
return <WrappedComponent {...props} {...(props as T)} />;
}
ComponentWithTooltip.displayName = `withTooltip(${displayName})`;
return ComponentWithTooltip;
}

View File

@ -1,3 +1 @@
export { Checkbox } from "./Checkbox";
export { withTooltip } from "./Tooltip";
export { default as Button } from "./Button";

View File

@ -19,7 +19,7 @@ import "./polyfills";
import GlobalStyles from "globalStyles";
// enable autofreeze only in development
import { setAutoFreeze } from "immer";
import AppErrorBoundary from "AppErrorBoundry";
import AppErrorBoundary from "./AppErrorBoundry";
const shouldAutoFreeze = process.env.NODE_ENV === "development";
setAutoFreeze(shouldAutoFreeze);

View File

@ -6,7 +6,7 @@ import { AlignWidgetTypes } from "widgets/constants";
import { Colors } from "constants/Colors";
import { LabelPosition } from "components/constants";
import { FontStyleTypes } from "constants/WidgetConstants";
import { Checkbox } from "components/wds/Checkbox";
import { Checkbox } from "components/wds";
type StyledCheckboxContainerProps = {
isValid: boolean;

View File

@ -28,6 +28,6 @@
"noFallthroughCasesInSwitch": true
},
"include": [
"./src/**/*"
]
"./src/**/*",
],
}

File diff suppressed because it is too large Load Diff