chore: Yarn workspaces + Storybook + WDS (#20776)
This commit is contained in:
parent
05e806e2d3
commit
0696183b06
|
|
@ -14,7 +14,7 @@
|
|||
"api": "./src/api/",
|
||||
"assets": "./src/assets/",
|
||||
"sagas": "./src/sagas/",
|
||||
"@appsmith": "./src/ee",
|
||||
"@appsmith": "./src/ee"
|
||||
}
|
||||
}]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
2
app/client/.gitignore
vendored
2
app/client/.gitignore
vendored
|
|
@ -48,3 +48,5 @@ build-storybook.log
|
|||
TODO
|
||||
|
||||
/nginx
|
||||
|
||||
/public/static/wds/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
16
app/client/packages/storybook/.eslintrc.json
Normal file
16
app/client/packages/storybook/.eslintrc.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": [
|
||||
"../../.eslintrc.json",
|
||||
"plugin:storybook/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/*.stories.*"
|
||||
],
|
||||
"rules": {
|
||||
"import/no-anonymous-default-export": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
25
app/client/packages/storybook/.storybook/appsmith-theme.js
Normal file
25
app/client/packages/storybook/.storybook/appsmith-theme.js
Normal 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",
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
41
app/client/packages/storybook/.storybook/main.js
Normal file
41
app/client/packages/storybook/.storybook/main.js
Normal 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",
|
||||
},
|
||||
};
|
||||
74
app/client/packages/storybook/.storybook/manager-head.html
Normal file
74
app/client/packages/storybook/.storybook/manager-head.html
Normal 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>
|
||||
10
app/client/packages/storybook/.storybook/manager.js
Normal file
10
app/client/packages/storybook/.storybook/manager.js
Normal 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,
|
||||
},
|
||||
});
|
||||
17
app/client/packages/storybook/.storybook/preview.js
Normal file
17
app/client/packages/storybook/.storybook/preview.js
Normal 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];
|
||||
18
app/client/packages/storybook/.storybook/styles.css
Normal file
18
app/client/packages/storybook/.storybook/styles.css
Normal 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;
|
||||
}
|
||||
46
app/client/packages/storybook/package.json
Normal file
46
app/client/packages/storybook/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
3
app/client/packages/storybook/tsconfig.json
Normal file
3
app/client/packages/storybook/tsconfig.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
}
|
||||
18
app/client/packages/wds/package.json
Normal file
18
app/client/packages/wds/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
81
app/client/packages/wds/src/components/Button/Button.tsx
Normal file
81
app/client/packages/wds/src/components/Button/Button.tsx
Normal 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 };
|
||||
197
app/client/packages/wds/src/components/Button/index.styled.tsx
Normal file
197
app/client/packages/wds/src/components/Button/index.styled.tsx
Normal 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;
|
||||
}
|
||||
`;
|
||||
2
app/client/packages/wds/src/components/Button/index.tsx
Normal file
2
app/client/packages/wds/src/components/Button/index.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { Button } from "./Button";
|
||||
export { transformV1ButtonProps } from "./utils";
|
||||
27
app/client/packages/wds/src/components/Button/utils.ts
Normal file
27
app/client/packages/wds/src/components/Button/utils.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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",
|
||||
};
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;"}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { ButtonGroup } from "./ButtonGroup";
|
||||
|
|
@ -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;
|
||||
}
|
||||
`;
|
||||
15
app/client/packages/wds/src/components/Spinner/index.tsx
Normal file
15
app/client/packages/wds/src/components/Spinner/index.tsx
Normal 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 };
|
||||
31
app/client/packages/wds/src/components/Text/Text.tsx
Normal file
31
app/client/packages/wds/src/components/Text/Text.tsx
Normal 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;
|
||||
36
app/client/packages/wds/src/components/Text/index.styled.tsx
Normal file
36
app/client/packages/wds/src/components/Text/index.styled.tsx
Normal 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}
|
||||
`;
|
||||
1
app/client/packages/wds/src/components/Text/index.tsx
Normal file
1
app/client/packages/wds/src/components/Text/index.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Text } from "./Text";
|
||||
80
app/client/packages/wds/src/components/index.stories.mdx
Normal file
80
app/client/packages/wds/src/components/index.stories.mdx
Normal 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.
|
||||
We’ll 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
|
||||
|
||||

|
||||
|
||||
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, they’ve seen this UI in other applications before).
|
||||
|
||||
### **Cohesive**
|
||||
|
||||

|
||||
|
||||
Any element of WDS should seamlessly mix with other elements of the design system without breaking visual harmony.
|
||||
|
||||
### **Composable (Modular)**
|
||||
|
||||

|
||||
|
||||
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)**
|
||||
|
||||

|
||||
|
||||
Elements need to adapt to their context, look and work well on small and large screens.
|
||||
|
||||
### C**ustomisable (Themeable)**
|
||||
|
||||

|
||||
|
||||
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**
|
||||
|
||||

|
||||
|
||||
Color has many applications in UI design: drawing user’s 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**
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
Defined and consistent iconography and imagery sets the mood and tone for the UI.
|
||||
|
||||
### Layering / Depth
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
UI elements can have multiple interactive states. WDS will define how to deal with those in consistent and predictable fashion.
|
||||
3
app/client/packages/wds/src/index.ts
Normal file
3
app/client/packages/wds/src/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { Button } from "./components/Button";
|
||||
export { createTokens } from "./utils/createTokens";
|
||||
export { fontMetricsMap, createGlobalFontStack } from "./utils/typography";
|
||||
0
app/client/packages/wds/src/styles/globals.css
Normal file
0
app/client/packages/wds/src/styles/globals.css
Normal file
64
app/client/packages/wds/src/styles/tokens/raw.css
Normal file
64
app/client/packages/wds/src/styles/tokens/raw.css
Normal 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);
|
||||
}
|
||||
|
||||
11
app/client/packages/wds/src/styles/tokens/semantic.css
Normal file
11
app/client/packages/wds/src/styles/tokens/semantic.css
Normal 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);
|
||||
}
|
||||
86
app/client/packages/wds/src/utils/colors.ts
Normal file
86
app/client/packages/wds/src/utils/colors.ts
Normal 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");
|
||||
}
|
||||
};
|
||||
54
app/client/packages/wds/src/utils/createTokens.ts
Normal file
54
app/client/packages/wds/src/utils/createTokens.ts
Normal 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};
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
79
app/client/packages/wds/src/utils/typography.ts
Normal file
79
app/client/packages/wds/src/utils/typography.ts
Normal 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",
|
||||
});
|
||||
};
|
||||
3
app/client/packages/wds/tsconfig.json
Normal file
3
app/client/packages/wds/tsconfig.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default as withTooltip } from "./withTooltip";
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,3 +1 @@
|
|||
export { Checkbox } from "./Checkbox";
|
||||
export { withTooltip } from "./Tooltip";
|
||||
export { default as Button } from "./Button";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,6 @@
|
|||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
"./src/**/*",
|
||||
],
|
||||
}
|
||||
|
|
|
|||
6778
app/client/yarn.lock
6778
app/client/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user