Integrate tailwind css and create new button library (#2098)

Introduce tailwind as new frontend styling library to replace bulma in the longer run. Also create the first new ui library `ui-buttons` which will be the new standard for buttons ins SCM-Manager. In this library we reconsidered which types of buttons should be used to create a clean and consistent ui.

Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
Konstantin Schaper
2022-08-02 08:39:37 +02:00
committed by GitHub
parent 09beb8cd3b
commit 27dbcbf28d
67 changed files with 8592 additions and 7519 deletions

View File

@@ -0,0 +1,4 @@
{
"presets": ["@scm-manager/babel-preset"],
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}

View File

@@ -0,0 +1,57 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
const HtmlWebpackPlugin = require('html-webpack-plugin');
class RemoveThemesPlugin {
apply (compiler) {
compiler.hooks.compilation.tap('RemoveThemesPlugin', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
'RemoveThemesPlugin',
(data, cb) => {
// remove generated style-loader bundles from the page
// there should be a better way, which does not generate the bundles at all
// but for now it works
if (data.assets.js) {
data.assets.js = data.assets.js.filter(bundle => !bundle.startsWith("ui-theme-"))
.filter(bundle => !bundle.startsWith("runtime~ui-theme-"))
}
// remove css links to avoid conflicts with the themes
// so we remove all and add our own via preview-head.html
if (data.assets.css) {
data.assets.css = data.assets.css.filter(css => !css.startsWith("ui-theme-"))
}
// Tell webpack to move on
cb(null, data)
}
)
})
}
}
module.exports = RemoveThemesPlugin

View File

@@ -0,0 +1,27 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,109 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
const path = require("path");
const fs = require("fs");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const RemoveThemesPlugin = require("./RemoveThemesPlugin");
const ReactDOM = require("react-dom");
const root = path.resolve("..");
const themedir = path.join(root, "ui-styles", "src");
ReactDOM.createPortal = (node) => node;
const themes = fs
.readdirSync(themedir)
.map((filename) => path.parse(filename))
.filter((p) => p.ext === ".scss")
.reduce((entries, current) => ({ ...entries, [`ui-theme-${current.name}`]: path.join(themedir, current.base) }), {});
module.exports = {
typescript: { reactDocgen: false },
core: {
builder: "webpack5",
},
stories: ["../docs/**/*.stories.mdx", "../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"storybook-addon-i18next",
"storybook-addon-themes",
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/addon-a11y",
"storybook-addon-pseudo-states"
],
framework: "@storybook/react",
webpackFinal: async (config) => {
// add our themes to webpack entry points
config.entry = {
main: config.entry,
...themes,
};
// create separate css files for our themes
config.plugins.push(
new MiniCssExtractPlugin({
filename: "[name].css",
ignoreOrder: false,
})
);
config.module.rules.push({
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
});
config.module.rules.push({
test: /\.css$/,
use: [
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: {
tailwindcss: { config: require("./tailwind.config") },
autoprefixer: {},
},
},
},
},
],
include: path.resolve(__dirname, "../"),
});
// the html-webpack-plugin adds the generated css and js files to the iframe,
// which overrides our manually loaded css files.
// So we use a custom plugin which uses a hook of html-webpack-plugin
// to filter our themes from the output.
config.plugins.push(new RemoveThemesPlugin());
// force cjs instead of esm
// https://github.com/tannerlinsley/react-query/issues/3513
config.resolve.alias["react-query/devtools"] = require.resolve("react-query/devtools");
return config;
},
};

View File

@@ -0,0 +1,26 @@
<!--
MIT License
Copyright (c) 2020-present Cloudogu GmbH and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<link id="ui-theme" data-theme="light" rel="stylesheet" type="text/css" href="/ui-theme-light.css">

View File

@@ -0,0 +1,97 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import "./index.css";
import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import { withI18next } from "storybook-addon-i18next";
import React, {useEffect} from "react";
import withApiProvider from "./withApiProvider";
import { withThemes } from 'storybook-addon-themes/react';
let i18n = i18next;
// only use fetch backend for storybook
// and not for storyshots
if (!process.env.JEST_WORKER_ID) {
const Backend = require("i18next-fetch-backend");
i18n = i18n.use(Backend.default);
}
i18n.use(initReactI18next).init({
whitelist: ["en", "de", "es"],
lng: "en",
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
react: {
useSuspense: false,
},
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
init: {
credentials: "same-origin",
},
},
});
export const decorators = [
withI18next({
i18n,
languages: {
en: "English",
de: "Deutsch",
es: "Spanisch",
},
}),
withApiProvider,
withThemes
];
const Decorator = ({children, themeName}) => {
useEffect(() => {
const link = document.querySelector("#ui-theme");
if (link && link["data-theme"] !== themeName) {
link.href = `ui-theme-${themeName}.css`;
link["data-theme"] = themeName;
}
}, [themeName]);
return <>{children}</>
};
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
themes: {
Decorator,
clearable: false,
default: "light",
list: [
{ name: "light", color: "#fff" },
{ name: "highcontrast", color: "#050514" },
{ name: "dark", color: "#121212" },
],
}
};

View File

@@ -0,0 +1,33 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
const path = require("path");
module.exports = {
presets: [
// eslint-disable-next-line global-require,import/no-extraneous-dependencies
require("@scm-manager/ui-scripts/src/tailwind.config"),
],
content: [path.join(__dirname, "../{src,docs}/**/*.{tsx,mdx}")],
};

View File

@@ -0,0 +1,45 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as React from "react";
import { ApiProvider } from "@scm-manager/ui-api";
const withApiProvider = (storyFn) => {
return React.createElement(ApiProvider, {
index: {
version: "x.y.z",
_links: {}
},
me: {
name: "trillian",
displayName: "Trillian McMillan",
mail: "trillian@hitchhiker.com",
groups: [],
_links: {}
},
devtools: false,
children: storyFn()
});
}
export default withApiProvider;

View File

@@ -0,0 +1,64 @@
import { Meta } from "@storybook/addon-docs";
import { Button } from "../src";
<Meta title="Introduction"/>
# Buttons
The `@scm-manager/ui-buttons` library provides [atoms](https://atomicdesign.bradfrost.com/chapter-2/#atoms) implemented
as minimal wrappers around native html elements styled to match the general SCM-Manager aesthetic.
## Components
There are three actionable components available. Styling is consistent amongst them and all have the required `variant` property.
1. [Button](?path=/story/components--button)
2. [Link Button](?path=/story/components--link-button)
3. [External Link Button](?path=/story/components--external-link-button)
## Usage
Actionable components serve a dedicated purpose. It is therefore important to know when and how to use them.
### Variants
There are four variants available to each of the three button types, varying in importance.
<table>
<thead>
<tr>
<th>Emphasis</th>
<th>Button Variant</th>
<th>Usage Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td>Very High</td>
<td><Button variant="signal" className="w-full">Signal</Button></td>
<td>Destructive actions</td>
</tr>
<tr>
<td>High</td>
<td><Button variant="primary" className="w-full">Primary</Button></td>
<td>Form submit</td>
</tr>
<tr>
<td>Normal</td>
<td><Button variant="secondary" className="w-full">Secondary</Button></td>
<td>Cancel action in dialog</td>
</tr>
<tr>
<td>Low</td>
<td><Button variant="tertiary" className="w-full">Tertiary</Button></td>
<td>Circumstantially relevant action on page with many actionable elements</td>
</tr>
</tbody>
</table>
## Content
Buttons exclusively contain text and no icons. Icons tend to be ambiguous and not always applicable which leads to inconsistent
and cluttered layouts.
Button text should be short, concise and describe the action performed.

View File

@@ -0,0 +1,22 @@
import { Meta, Story } from "@storybook/addon-docs";
import { Button } from "../src";
<Meta title="Usage" parameters={{
storyshots: { disable: true }
}} />
In confirmation dialogs, there are two actions.<br/>
One to cancel the current process and one to confirm it.<br/>
Aborting is always the secondary action, confirmation always the primary.
Focus is always on the cancelling action.
<Story name="Confirmation Dialog">
<div className="max-w-2xl rounded border p-4">
<h4 className="mb-2 font-bold">Delete User</h4>
<p>Do you really want to delete this user ?</p>
<div className="flex justify-end gap-2">
<Button variant="secondary">Cancel</Button>
<Button variant="primary">Delete</Button>
</div>
</div>
</Story>

View File

@@ -0,0 +1,85 @@
{
"name": "@scm-manager/ui-buttons",
"version": "2.37.3-SNAPSHOT",
"private": true,
"main": "build/index.js",
"module": "build/index.mjs",
"types": "build/index.d.ts",
"files": [
"build"
],
"scripts": {
"build": "tsup ./src/index.ts -d build --format esm,cjs --dts",
"dev": "tsup ./src/index.ts -d build --format esm,cjs --dts --watch",
"lint": "eslint src",
"typecheck": "tsc",
"storybook": "start-storybook -p 6006 -s ../ui-webapp/public",
"build-storybook": "build-storybook",
"image-snapshots": "jest \"image-snapshot.test.ts\"",
"a11y-check": "jest \"a11y.test.ts\"",
"depcheck": "depcheck --ignores=@scm-manager/prettier-config,@scm-manager/tsconfig,@babel/core,sass-loader,autoprefixer,babel-loader,postcss-loader,tailwindcss,storybook-addon-*,@storybook/*,webpack"
},
"peerDependencies": {
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router-dom": "^5.3.1",
"classnames": "^2.2.6"
},
"devDependencies": {
"@scm-manager/ui-scripts": "2.37.3-SNAPSHOT",
"@scm-manager/ui-styles": "2.37.3-SNAPSHOT",
"@scm-manager/ui-api": "2.37.3-SNAPSHOT",
"@scm-manager/eslint-config": "^2.16.0",
"@babel/core": "^7.17.8",
"@scm-manager/tsconfig": "^2.12.0",
"@storybook/addon-essentials": "^6.4.20",
"@storybook/addon-interactions": "^6.4.20",
"@storybook/addon-a11y": "^6.4.20",
"@storybook/addon-links": "^6.4.20",
"@storybook/builder-webpack5": "^6.4.20",
"@storybook/manager-webpack5": "^6.4.20",
"@storybook/react": "^6.4.20",
"@storybook/addon-storyshots-puppeteer": "^6.4.20",
"@storybook/addon-storyshots": "^6.4.20",
"@storybook/testing-library": "^0.0.9",
"jest-transform-css": "^4.0.1",
"puppeteer": "^15.5.0",
"storybook-addon-pseudo-states": "^1.15.1",
"storybook-react-router": "^1.0.8",
"@types/storybook-react-router": "^1.0.2",
"sass-loader": "^12.3.0",
"storybook-addon-themes": "^6.1.0",
"autoprefixer": "^10.4.4",
"babel-loader": "^8.2.4",
"postcss": "^8.4.12",
"postcss-loader": "^6.2.1",
"tailwindcss": "^3.0.23",
"webpack": "5",
"tsup": "^6.1.2",
"mini-css-extract-plugin": "^1.6.2",
"html-webpack-plugin": "^5.5.0",
"react-query": "^3.25.1",
"i18next": "^19.9.2",
"react-i18next": "^10.13.2",
"i18next-fetch-backend": "^2.3.1"
},
"babel": {
"presets": [
"@scm-manager/babel-preset"
]
},
"jest": {
"transform": {
"^.+\\.[tj]sx?$": "babel-jest",
"^.+\\.(css|less|scss)$": "jest-transform-css",
"^.+\\.mdx?$": "@storybook/addon-docs/jest-transform-mdx"
}
},
"prettier": "@scm-manager/prettier-config",
"eslintConfig": {
"extends": "@scm-manager/eslint-config"
},
"publishConfig": {
"access": "restricted"
}
}

View File

@@ -0,0 +1,30 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,34 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import initStoryshots from "@storybook/addon-storyshots";
import { axeTest } from "@storybook/addon-storyshots-puppeteer";
import path from "path";
initStoryshots({
suite: "A11y checks",
test: axeTest({
storybookUrl: `file://${path.resolve(__dirname, "../storybook-static")}`,
}),
storyNameRegex: /High-Contrast States/,
});

View File

@@ -0,0 +1,91 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import {
Button as ButtonComponent,
ButtonVariantList,
ButtonVariants,
ExternalLinkButton as ExternalLinkButtonComponent,
LinkButton as LinkButtonComponent,
} from "./button";
import StoryRouter from "storybook-react-router";
import { StoryFn } from "@storybook/react";
type ExtractProps<T> = T extends React.ComponentType<infer U> ? U : never;
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: "Components",
component: null,
subcomponents: {
Button: ButtonComponent,
LinkButton: LinkButtonComponent,
ExternalLinkButton: ExternalLinkButtonComponent,
},
argTypes: {
variant: {
options: ButtonVariantList,
control: { type: "select" },
},
},
decorators: [StoryRouter()],
parameters: {
storyshots: { disable: true },
},
};
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const ButtonTemplate: StoryFn<ExtractProps<typeof ButtonComponent>> = (args) => <ButtonComponent {...args} />;
const LinkButtonTemplate: StoryFn<ExtractProps<typeof LinkButtonComponent>> = (args) => (
<LinkButtonComponent {...args} />
);
const ExternalLinkButtonTemplate: StoryFn<ExtractProps<typeof ExternalLinkButtonComponent>> = (args) => (
<ExternalLinkButtonComponent {...args} />
);
export const Button = ButtonTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
Button.args = {
children: "Button",
variant: ButtonVariants.PRIMARY,
disabled: false,
};
export const LinkButton = LinkButtonTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
LinkButton.args = {
children: "Link Button",
to: "/repos",
variant: ButtonVariants.PRIMARY,
};
export const ExternalLinkButton = ExternalLinkButtonTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
ExternalLinkButton.args = {
children: "External Link Button",
href: "https://scm-manager.org",
variant: ButtonVariants.PRIMARY,
};

View File

@@ -0,0 +1,73 @@
import { Meta, Story } from "@storybook/addon-docs";
import { Button, ButtonVariantList } from "./button";
<Meta title="Tests"/>
<Story name="Light States" parameters={{
pseudo: {
hover: ButtonVariantList.map(variant => `#${variant}-Hover`),
focus: ButtonVariantList.map(variant => `#${variant}-Focus`),
active: ButtonVariantList.map(variant => `#${variant}-Active`),
},
themes: {
default: 'light',
},
}}>
<table className="border-separate border-spacing-4">
<tr>
<th>STATE</th>
{ButtonVariantList.map(variant => <th>{variant.toUpperCase()}</th>)}
</tr>
{["Normal", "Hover", "Active", "Focus", "Disabled"].map(state => <tr>
<td>{state}</td>
{ButtonVariantList.map(variant => <td><Button id={`${variant}-${state}`} disabled={state === "Disabled"}
variant={variant}>Button</Button></td>)}
</tr>)}
</table>
</Story>
<Story name="Dark States" parameters={{
pseudo: {
hover: ButtonVariantList.map(variant => `#${variant}-Hover`),
focus: ButtonVariantList.map(variant => `#${variant}-Focus`),
active: ButtonVariantList.map(variant => `#${variant}-Active`),
},
themes: {
default: 'dark',
},
}}>
<table className="border-separate border-spacing-4">
<tr>
<th>STATE</th>
{ButtonVariantList.map(variant => <th>{variant.toUpperCase()}</th>)}
</tr>
{["Normal", "Hover", "Active", "Focus", "Disabled"].map(state => <tr>
<td>{state}</td>
{ButtonVariantList.map(variant => <td><Button id={`${variant}-${state}`} disabled={state === "Disabled"}
variant={variant}>Button</Button></td>)}
</tr>)}
</table>
</Story>
<Story name="High-Contrast States" parameters={{
pseudo: {
hover: ButtonVariantList.map(variant => `#${variant}-Hover`),
focus: ButtonVariantList.map(variant => `#${variant}-Focus`),
active: ButtonVariantList.map(variant => `#${variant}-Active`),
},
themes: {
default: 'highcontrast',
},
}}>
<table className="border-separate border-spacing-4">
<tr>
<th>STATE</th>
{ButtonVariantList.map(variant => <th>{variant.toUpperCase()}</th>)}
</tr>
{["Normal", "Hover", "Active", "Focus", "Disabled"].map(state => <tr>
<td>{state}</td>
{ButtonVariantList.map(variant => <td><Button id={`${variant}-${state}`} disabled={state === "Disabled"}
variant={variant}>Button</Button></td>)}
</tr>)}
</table>
</Story>

View File

@@ -0,0 +1,109 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes } from "react";
import { Link as ReactRouterLink, LinkProps as ReactRouterLinkProps } from "react-router-dom";
import classNames from "classnames";
export const ButtonVariants = {
PRIMARY: "primary",
SECONDARY: "secondary",
TERTIARY: "tertiary",
SIGNAL: "signal",
} as const;
export const ButtonVariantList = Object.values(ButtonVariants);
type ButtonVariant = typeof ButtonVariants[keyof typeof ButtonVariants];
const BASE_BUTTON_CLASSES = classNames(
"inline-block whitespace-nowrap rounded border py-2 px-6 text-center font-semibold focus:z-10 focus:outline focus:outline-offset-2 focus:outline-purple-500 disabled:cursor-not-allowed"
);
const DEFAULT_BUTTON_CLASSES = classNames(
"border-gray-200 hover:border-gray-400 active:shadow-inner disabled:hover:border-gray-200 disabled:active:shadow-none"
);
const PRIMARY_BUTTON_CLASSES = classNames(
"border-transparent bg-primary text-primary-contrast hover:bg-primary-hover active:bg-primary-active disabled:bg-primary-disabled disabled:text-primary-disabled-contrast "
);
const SECONDARY_BUTTON_CLASSES = classNames(
"border-primary text-primary hover:border-primary-hover hover:text-primary-hover active:border-primary-active active:text-primary-active disabled:border-primary-disabled disabled:text-primary-disabled"
);
const TERTIARY_BUTTON_CLASSES = classNames(
"border-transparent text-primary hover:text-primary-hover active:text-primary-active disabled:text-primary-disabled"
);
const SIGNAL_BUTTON_CLASSES = classNames(
"border-transparent bg-signal text-signal-contrast hover:bg-signal-hover hover:text-signal-hover-contrast active:bg-signal-active active:text-signal-active-contrast disabled:bg-signal-disabled disabled:text-signal-disabled-contrast"
);
const createButtonClasses = (variant?: ButtonVariant) =>
classNames(BASE_BUTTON_CLASSES, {
[DEFAULT_BUTTON_CLASSES]: !variant,
[PRIMARY_BUTTON_CLASSES]: variant === "primary",
[SECONDARY_BUTTON_CLASSES]: variant === "secondary",
[TERTIARY_BUTTON_CLASSES]: variant === "tertiary",
[SIGNAL_BUTTON_CLASSES]: variant === "signal",
});
type BaseButtonProps = {
variant: ButtonVariant;
};
type ButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
/**
* Styled html button
*/
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, children, ...props }, ref) => (
<button {...props} className={classNames(createButtonClasses(variant), className)} ref={ref}>
{children}
</button>
)
);
type LinkButtonProps = BaseButtonProps & ReactRouterLinkProps;
/**
* Styled react router link
*/
export const LinkButton = React.forwardRef<HTMLAnchorElement, LinkButtonProps>(
({ className, variant, children, ...props }, ref) => (
<ReactRouterLink {...props} className={classNames(createButtonClasses(variant), className)} ref={ref}>
{children}
</ReactRouterLink>
)
);
type ExternalLinkButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLAnchorElement>;
/**
* Styled html anchor
*/
export const ExternalLinkButton = React.forwardRef<HTMLAnchorElement, ExternalLinkButtonProps>(
({ className, variant, children, ...props }, ref) => (
<a {...props} className={classNames(createButtonClasses(variant), className)} ref={ref}>
{children}
</a>
)
);

View File

@@ -0,0 +1,33 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import path from "path";
import initStoryshots from "@storybook/addon-storyshots";
import { imageSnapshot } from "@storybook/addon-storyshots-puppeteer";
initStoryshots({
suite: "Image snapshots",
test: imageSnapshot({
storybookUrl: `file://${path.resolve(__dirname, "../storybook-static")}`,
})
});

View File

@@ -0,0 +1,26 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,27 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import "./index.css";
export { Button, LinkButton, ExternalLinkButton, ButtonVariants } from "./button";

View File

@@ -0,0 +1,34 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
const path = require("path");
module.exports = {
presets: [
// eslint-disable-next-line global-require,import/no-extraneous-dependencies
require("@scm-manager/ui-styles/src/tailwind.config.preset"),
],
content: [path.join(__dirname, "src/**/*.tsx")],
important: true,
};

View File

@@ -0,0 +1,7 @@
{
"extends": "@scm-manager/tsconfig",
"include": [
"./src",
"./docs"
]
}