Merge pull request #1731 from SkynetLabs/dashboard-v2-move-storybook-components
Dashboard v2 move storybook components
This commit is contained in:
commit
29d4df0be6
|
@ -0,0 +1,22 @@
|
||||||
|
name: Build Storybook - packages/dashboard-v2
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: packages/dashboard-v2
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
|
- run: yarn install
|
||||||
|
- run: yarn build-storybook
|
||||||
|
- name: "Deploy to Skynet"
|
||||||
|
uses: skynetlabs/deploy-to-skynet-action@v2
|
||||||
|
with:
|
||||||
|
upload-dir: packages/dashboard-v2/storybook-build
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -1,3 +1,4 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
.cache/
|
.cache/
|
||||||
public
|
public/
|
||||||
|
storybook-build/
|
||||||
|
|
|
@ -2,5 +2,5 @@ module.exports = {
|
||||||
globals: {
|
globals: {
|
||||||
__PATH_PREFIX__: true,
|
__PATH_PREFIX__: true,
|
||||||
},
|
},
|
||||||
extends: "react-app",
|
extends: ["react-app", "plugin:storybook/recommended"],
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
.cache/
|
.cache/
|
||||||
public
|
public/
|
||||||
|
storybook-build/
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
.cache/
|
.cache/
|
||||||
public/
|
public/
|
||||||
|
storybook-build/
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
module.exports = {
|
||||||
|
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||||
|
addons: [
|
||||||
|
"@storybook/addon-links",
|
||||||
|
"@storybook/addon-essentials",
|
||||||
|
{
|
||||||
|
name: "@storybook/addon-postcss",
|
||||||
|
options: {
|
||||||
|
postcssLoaderOptions: {
|
||||||
|
implementation: require("postcss"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
core: {
|
||||||
|
builder: "webpack5",
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,20 @@
|
||||||
|
import "tailwindcss/tailwind.css";
|
||||||
|
import "@fontsource/sora/300.css"; // light
|
||||||
|
import "@fontsource/sora/400.css"; // normal
|
||||||
|
import "@fontsource/sora/500.css"; // medium
|
||||||
|
import "@fontsource/sora/600.css"; // semibold
|
||||||
|
import "@fontsource/source-sans-pro/400.css"; // normal
|
||||||
|
import "@fontsource/source-sans-pro/600.css"; // semibold
|
||||||
|
|
||||||
|
import "../src/styles/global.css";
|
||||||
|
|
||||||
|
export const parameters = {
|
||||||
|
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: "fullscreen",
|
||||||
|
};
|
|
@ -14,32 +14,47 @@
|
||||||
"serve": "gatsby serve",
|
"serve": "gatsby serve",
|
||||||
"clean": "gatsby clean",
|
"clean": "gatsby clean",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"prettier": "prettier ."
|
"prettier": "prettier .",
|
||||||
|
"storybook": "start-storybook -p 6006",
|
||||||
|
"build-storybook": "build-storybook -o storybook-build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/sora": "^4.5.0",
|
"@fontsource/sora": "^4.5.3",
|
||||||
"@fontsource/source-sans-pro": "^4.5.1",
|
"@fontsource/source-sans-pro": "^4.5.3",
|
||||||
"gatsby": "^4.6.2",
|
"gatsby": "^4.6.2",
|
||||||
|
"gatsby-plugin-postcss": "^5.7.0",
|
||||||
|
"postcss": "^8.4.6",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"tailwindcss": "^3.0.22"
|
"react-use": "^17.3.2",
|
||||||
|
"tailwindcss": "^3.0.23"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.17.4",
|
||||||
|
"@storybook/addon-actions": "^6.4.19",
|
||||||
|
"@storybook/addon-essentials": "^6.4.19",
|
||||||
|
"@storybook/addon-interactions": "^6.4.19",
|
||||||
|
"@storybook/addon-links": "^6.4.19",
|
||||||
|
"@storybook/addon-postcss": "^2.0.0",
|
||||||
|
"@storybook/builder-webpack5": "^6.4.19",
|
||||||
|
"@storybook/manager-webpack5": "^6.4.19",
|
||||||
|
"@storybook/react": "^6.4.19",
|
||||||
|
"@storybook/testing-library": "^0.0.9",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
|
"babel-loader": "^8.2.3",
|
||||||
"babel-plugin-styled-components": "^2.0.2",
|
"babel-plugin-styled-components": "^2.0.2",
|
||||||
"eslint": "^8.9.0",
|
"eslint": "^8.9.0",
|
||||||
"eslint-config-react-app": "^7.0.0",
|
"eslint-config-react-app": "^7.0.0",
|
||||||
|
"eslint-plugin-storybook": "^0.5.6",
|
||||||
"gatsby-plugin-alias-imports": "^1.0.5",
|
"gatsby-plugin-alias-imports": "^1.0.5",
|
||||||
"gatsby-plugin-image": "^2.6.0",
|
"gatsby-plugin-image": "^2.6.0",
|
||||||
"gatsby-plugin-postcss": "^5.7.0",
|
|
||||||
"gatsby-plugin-react-helmet": "^5.6.0",
|
"gatsby-plugin-react-helmet": "^5.6.0",
|
||||||
"gatsby-plugin-sharp": "^4.6.0",
|
"gatsby-plugin-sharp": "^4.6.0",
|
||||||
"gatsby-plugin-styled-components": "^5.7.0",
|
"gatsby-plugin-styled-components": "^5.7.0",
|
||||||
"gatsby-source-filesystem": "^4.6.0",
|
"gatsby-source-filesystem": "^4.6.0",
|
||||||
"gatsby-transformer-sharp": "^4.6.0",
|
"gatsby-transformer-sharp": "^4.6.0",
|
||||||
"postcss": "^8.4.6",
|
|
||||||
"prettier": "2.5.1",
|
"prettier": "2.5.1",
|
||||||
"react-is": "^17.0.2",
|
"react-is": "^17.0.2",
|
||||||
"styled-components": "^5.3.3"
|
"styled-components": "^5.3.3"
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [require("tailwindcss"), require("autoprefixer")],
|
plugins: [require("tailwindcss/nesting"), require("tailwindcss"), require("autoprefixer")],
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary UI component for user interaction
|
||||||
|
*/
|
||||||
|
export const Button = ({ primary, label, ...props }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`min-w-button min-h-button rounded-full font-sans uppercase tracking-wide text-button
|
||||||
|
${primary ? "bg-primary" : "bg-white border-2 border-black"}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Button.propTypes = {
|
||||||
|
/**
|
||||||
|
* Is this the principal call to action on the page?
|
||||||
|
*/
|
||||||
|
primary: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* What background color to use
|
||||||
|
*/
|
||||||
|
backgroundColor: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Button contents
|
||||||
|
*/
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* Optional click handler
|
||||||
|
*/
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
Button.defaultProps = {
|
||||||
|
primary: false,
|
||||||
|
onClick: undefined,
|
||||||
|
};
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Button } from "./Button";
|
||||||
|
|
||||||
|
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/Button",
|
||||||
|
component: Button,
|
||||||
|
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||||
|
argTypes: {
|
||||||
|
backgroundColor: { control: "color" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||||
|
const Template = (args) => <Button {...args} />;
|
||||||
|
|
||||||
|
export const Primary = Template.bind({});
|
||||||
|
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
Primary.args = {
|
||||||
|
primary: true,
|
||||||
|
label: "Button",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Secondary = Template.bind({});
|
||||||
|
Secondary.args = {
|
||||||
|
label: "Button",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Large = Template.bind({});
|
||||||
|
Large.args = {
|
||||||
|
size: "large",
|
||||||
|
label: "Button",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Small = Template.bind({});
|
||||||
|
Small.args = {
|
||||||
|
size: "small",
|
||||||
|
label: "Button",
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./Button";
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { useClickAway } from "react-use";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styled, { css, keyframes } from "styled-components";
|
||||||
|
|
||||||
|
import { ChevronDownIcon } from "../Icons";
|
||||||
|
|
||||||
|
const dropDown = keyframes`
|
||||||
|
0% {
|
||||||
|
transform: scaleY(0);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: scaleY(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaleY(1);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Container = styled.div.attrs({ className: `relative inline-flex` })``;
|
||||||
|
|
||||||
|
const Trigger = styled.button.attrs({
|
||||||
|
className: "flex items-center",
|
||||||
|
})``;
|
||||||
|
|
||||||
|
const TriggerIcon = styled(ChevronDownIcon).attrs({
|
||||||
|
className: "transition-transform text-primary",
|
||||||
|
})`
|
||||||
|
transform: ${({ open }) => (open ? "rotateX(180deg)" : "none")};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Flyout = styled.div.attrs(({ open }) => ({
|
||||||
|
className: `absolute top-full right-0 p-0
|
||||||
|
border rounded border-palette-100
|
||||||
|
bg-white shadow-md shadow-palette-200/50
|
||||||
|
${open ? "visible" : "invisible"}`,
|
||||||
|
}))`
|
||||||
|
animation: ${({ open }) =>
|
||||||
|
open
|
||||||
|
? css`
|
||||||
|
${dropDown} 0.1s ease-in-out
|
||||||
|
`
|
||||||
|
: "none"};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DropdownMenu = ({ title, children }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const menuRef = useRef();
|
||||||
|
|
||||||
|
useClickAway(menuRef, () => setOpen(false));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container ref={menuRef}>
|
||||||
|
<Trigger onClick={() => setOpen((open) => !open)}>
|
||||||
|
{title} <TriggerIcon open={open} />
|
||||||
|
</Trigger>
|
||||||
|
<Flyout open={open}>{children}</Flyout>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DropdownMenu.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Panel } from "../Panel";
|
||||||
|
import { DropdownMenu, DropdownMenuLink } from ".";
|
||||||
|
import { CogIcon, LockClosedIcon } from "../Icons";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/DropdownMenu",
|
||||||
|
component: DropdownMenu,
|
||||||
|
subcomponents: {
|
||||||
|
DropdownMenuLink,
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Panel style={{ margin: 50, textAlign: "center" }}>
|
||||||
|
<Story />
|
||||||
|
</Panel>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NavigationDropdown = () => (
|
||||||
|
<DropdownMenu title="My account">
|
||||||
|
<DropdownMenuLink href="/settings" icon={CogIcon} label="Settings" active />
|
||||||
|
<DropdownMenuLink href="/logout" icon={LockClosedIcon} label="Log out" />
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
|
@ -0,0 +1,25 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const DropdownLink = styled.a.attrs({
|
||||||
|
className: `m-0 border-t border-palette-200/50 h-[60px]
|
||||||
|
whitespace-nowrap transition-colors text-current
|
||||||
|
hover:bg-palette-100/50 flex items-center
|
||||||
|
pr-8 pl-6 py-4 gap-4 first:border-0`,
|
||||||
|
})``;
|
||||||
|
|
||||||
|
export const DropdownMenuLink = ({ active, icon: Icon, label, ...props }) => (
|
||||||
|
<>
|
||||||
|
<DropdownLink {...props}>
|
||||||
|
{Icon ? <Icon className={active ? "text-primary" : "text-current"} /> : null}
|
||||||
|
{label}
|
||||||
|
</DropdownLink>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
DropdownMenuLink.propTypes = {
|
||||||
|
label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]).isRequired,
|
||||||
|
active: PropTypes.bool,
|
||||||
|
icon: PropTypes.func,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./DropdownMenu";
|
||||||
|
export * from "./DropdownMenuLink";
|
|
@ -0,0 +1,60 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary UI component for user interaction
|
||||||
|
*/
|
||||||
|
export const IconButton = ({ primary, size, icon, ...props }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${
|
||||||
|
size === "small"
|
||||||
|
? "h-iconButtonSm w-buttonIconSm"
|
||||||
|
: size === "large"
|
||||||
|
? "h-iconButtonLg w-iconButtonLg"
|
||||||
|
: "w-iconButton h-iconButton"
|
||||||
|
} rounded-full
|
||||||
|
inline-flex justify-center items-center
|
||||||
|
${primary ? "bg-primary" : null}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
size === "small"
|
||||||
|
? "h-buttonIconSm w-buttonIconSm"
|
||||||
|
: size === "large"
|
||||||
|
? "h-buttonIconLg w-buttonIconLg"
|
||||||
|
: "h-buttonIcon w-buttonIcon"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
IconButton.propTypes = {
|
||||||
|
/**
|
||||||
|
* Is this the principal call to action on the page?
|
||||||
|
*/
|
||||||
|
primary: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* How large should the button be?
|
||||||
|
*/
|
||||||
|
size: PropTypes.oneOf(["small", "medium", "large"]),
|
||||||
|
/**
|
||||||
|
* Icon component
|
||||||
|
*/
|
||||||
|
icon: PropTypes.element.isRequired,
|
||||||
|
/**
|
||||||
|
* Optional click handler
|
||||||
|
*/
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
IconButton.defaultProps = {
|
||||||
|
backgroundColor: null,
|
||||||
|
primary: false,
|
||||||
|
size: "medium",
|
||||||
|
onClick: undefined,
|
||||||
|
};
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { IconButton } from "./IconButton";
|
||||||
|
import { ArrowRightIcon } from "../Icons";
|
||||||
|
|
||||||
|
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/IconButton",
|
||||||
|
component: IconButton,
|
||||||
|
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||||
|
argTypes: {
|
||||||
|
backgroundColor: { control: "color" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||||
|
const Template = (args) => <IconButton {...args} />;
|
||||||
|
|
||||||
|
export const Primary = Template.bind({});
|
||||||
|
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
Primary.args = {
|
||||||
|
primary: true,
|
||||||
|
icon: <ArrowRightIcon />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Secondary = Template.bind({});
|
||||||
|
Secondary.args = {
|
||||||
|
icon: <ArrowRightIcon />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Large = Template.bind({});
|
||||||
|
Large.args = {
|
||||||
|
size: "large",
|
||||||
|
icon: <ArrowRightIcon />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Small = Template.bind({});
|
||||||
|
Small.args = {
|
||||||
|
size: "small",
|
||||||
|
icon: <ArrowRightIcon />,
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./IconButton";
|
|
@ -0,0 +1,46 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary UI component for user interaction
|
||||||
|
*/
|
||||||
|
export const IconButtonText = ({ primary, label, icon, ...props }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex justify-center items-center w-iconButtonTextWidth py-iconButtonTextY`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={`h-buttonTextIcon w-buttonTextIcon ${primary ? "text-primary" : "text-palette-600"}`}>{icon}</div>
|
||||||
|
<p
|
||||||
|
className={"ml-iconButtonTextTextLeft tracking-wide text-iconButtonText font-sans font-light text-palette-600"}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
IconButtonText.propTypes = {
|
||||||
|
/**
|
||||||
|
* Is this the principal call to action on the page?
|
||||||
|
*/
|
||||||
|
primary: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Button Label
|
||||||
|
*/
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* Icon component
|
||||||
|
*/
|
||||||
|
icon: PropTypes.element.isRequired,
|
||||||
|
/**
|
||||||
|
* Optional click handler
|
||||||
|
*/
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
IconButtonText.defaultProps = {
|
||||||
|
primary: false,
|
||||||
|
label: "",
|
||||||
|
onClick: undefined,
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { IconButtonText } from "./IconButtonText";
|
||||||
|
import { CogIcon } from "../Icons";
|
||||||
|
|
||||||
|
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/IconButtonText",
|
||||||
|
component: IconButtonText,
|
||||||
|
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||||
|
argTypes: {
|
||||||
|
backgroundColor: { control: "color" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||||
|
const Template = (args) => <IconButtonText {...args} />;
|
||||||
|
|
||||||
|
export const Primary = Template.bind({});
|
||||||
|
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
Primary.args = {
|
||||||
|
primary: true,
|
||||||
|
label: "Settings",
|
||||||
|
icon: <CogIcon />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Secondary = Template.bind({});
|
||||||
|
Secondary.args = {
|
||||||
|
label: "Settings",
|
||||||
|
icon: <CogIcon />,
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./IconButtonText";
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Panel } from "../Panel";
|
||||||
|
import * as icons from ".";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/Icons",
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div style={{ margin: 50 }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultSizeIcon = () => <icons.SkynetLogoIcon />;
|
||||||
|
|
||||||
|
export const LargeIcon = () => <icons.SkynetLogoIcon size={60} />;
|
||||||
|
|
||||||
|
export const AllIcons = () => {
|
||||||
|
const sizes = [24, 32, 60];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.entries(icons).map(([iconName, IconComponent]) => (
|
||||||
|
<Panel key={`panel-${iconName}`}>
|
||||||
|
<pre>{iconName}</pre>
|
||||||
|
|
||||||
|
<div style={{ padding: 10, border: "1px dashed #fafafa", display: "flex", alignItems: "center", gap: 50 }}>
|
||||||
|
{sizes.map((size) => (
|
||||||
|
<IconComponent key={`${iconName}-${size}`} size={size} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { withIconProps } from "../withIconProps";
|
||||||
|
|
||||||
|
export const ArrowRightIcon = withIconProps(({ size, ...props }) => (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path
|
||||||
|
d="M10 15h9.616L16.3 11.71l1.409-1.42 4.995 4.954a1 1 0 0 1 .09 1.32l-.084.094-5 5.046-1.42-1.408 3.265-3.295L10 17v-2z"
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="nonzero"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
));
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { withIconProps } from "../withIconProps";
|
||||||
|
|
||||||
|
export const ChevronDownIcon = withIconProps(({ size, ...props }) => (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path
|
||||||
|
d="M21.5 14.005 16.546 19 11.5 14"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="none"
|
||||||
|
fillRule="evenodd"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
));
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { withIconProps } from "../withIconProps";
|
||||||
|
|
||||||
|
export const CogIcon = withIconProps(({ size, ...props }) => (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path
|
||||||
|
d="M16 4a3 3 0 0 1 3 3v.086c.001.26.156.493.404.6a.647.647 0 0 0 .709-.123l.204-.195a3 3 0 0 1 4.1 4.38l-.052.051a.65.65 0 0 0-.13.717 1 1 0 0 1 .047.13l.012.06.045.062c.1.12.242.2.397.225l.094.007H25a3 3 0 0 1 0 6h-.086a.654.654 0 0 0-.6.404.647.647 0 0 0 .123.709l.195.204a3 3 0 0 1-4.38 4.1l-.051-.052a.654.654 0 0 0-.727-.126.649.649 0 0 0-.394.591V25a3 3 0 0 1-6 0v-.067c-.006-.266-.175-.502-.484-.618a.647.647 0 0 0-.709.122l-.204.195a3 3 0 0 1-4.1-4.38l.052-.051a.654.654 0 0 0 .126-.727.649.649 0 0 0-.591-.394H7a3 3 0 0 1 0-6h.067c.266-.006.502-.175.618-.484a.647.647 0 0 0-.122-.709l-.195-.204a3 3 0 0 1 4.38-4.1l.051.052a.65.65 0 0 0 .717.13 1 1 0 0 1 .13-.047l.06-.013.062-.044c.12-.1.2-.242.225-.397L13 7.17V7a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v.174a2.65 2.65 0 0 1-1.606 2.425A1 1 0 0 1 13 9.68l-.032.043a2.654 2.654 0 0 1-2.433-.537l-.142-.129-.06-.06a1 1 0 1 0-1.416 1.416l.068.068c.757.774.967 1.932.554 2.864l-.073.177A2.66 2.66 0 0 1 7.09 15.08H7a1 1 0 0 0 0 2h.174a2.646 2.646 0 0 1 2.42 1.596 2.654 2.654 0 0 1-.537 2.931l-.06.06a1 1 0 1 0 1.416 1.416l.068-.068c.774-.757 1.932-.967 2.864-.554l.177.073a2.66 2.66 0 0 1 1.558 2.376V25a1 1 0 0 0 2 0v-.174a2.646 2.646 0 0 1 1.596-2.42 2.654 2.654 0 0 1 2.931.537l.06.06a1 1 0 1 0 1.416-1.416l-.068-.068a2.646 2.646 0 0 1-.534-2.913A2.651 2.651 0 0 1 24.91 17H25a1 1 0 0 0 0-2h-.174a2.65 2.65 0 0 1-2.425-1.606A1 1 0 0 1 22.32 13l-.043-.032a2.654 2.654 0 0 1 .537-2.433l.129-.142.06-.06a1 1 0 1 0-1.416-1.416l-.068.068a2.646 2.646 0 0 1-2.913.534A2.651 2.651 0 0 1 17 7.09V7a1 1 0 0 0-1-1zm0 6a4 4 0 1 1 0 8 4 4 0 0 1 0-8zm0 2a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="nonzero"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
));
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { withIconProps } from "../withIconProps";
|
||||||
|
|
||||||
|
export const InfoIcon = withIconProps(({ size, ...props }) => (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path
|
||||||
|
d="M16 5c6.075 0 11 4.925 11 11s-4.925 11-11 11S5 22.075 5 16 9.925 5 16 5zm0 2a9 9 0 1 0 0 18 9 9 0 0 0 0-18zm0 6a1 1 0 0 1 .993.883L17 14v7a1 1 0 0 1-1.993.117L15 21v-7a1 1 0 0 1 1-1zm.01-3a1 1 0 0 1 .117 1.993L16 12a1 1 0 0 1-.117-1.993L16.01 10z"
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="nonzero"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
));
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { withIconProps } from "../withIconProps";
|
||||||
|
|
||||||
|
export const LockClosedIcon = withIconProps(({ size, ...props }) => (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path
|
||||||
|
d="M14.083 6.383A5 5 0 0 1 19.53 7.46a5.09 5.09 0 0 1 1.31 2.29 1.002 1.002 0 0 1-1.94.5 3.08 3.08 0 0 0-.78-1.38A3 3 0 0 0 13 11v2h8a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H11a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3v-2a5 5 0 0 1 3.083-4.617zM21 15H11a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1zm-4 2v5h-2v-5h2z"
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="nonzero"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
));
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { withIconProps } from "../withIconProps";
|
||||||
|
|
||||||
|
export const SkynetLogoIcon = withIconProps(({ size, ...props }) => (
|
||||||
|
<svg role="img" width={size} fill="#00C65E" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<title>Skynet</title>
|
||||||
|
<path d="m-.0004 6.4602 21.3893 11.297c.561.2935.6633 1.0532.1999 1.4846h-.011a10.0399 10.0399 0 0 1-2.2335 1.5307c-6.912 3.4734-14.9917-1.838-14.5438-9.5605l2.8601 1.9752c.856 4.508 5.6187 7.1094 9.8742 5.3932zm8.6477 3.1509 14.3661 5.6785a.8704.8704 0 0 1 .5197 1.0466v.0182c-.1537.5377-.7668.7938-1.2575.5252zm5.2896-7.4375c2.7093-.2325 6.0946.7869 8.1116 3.3871 1.699 2.1951 2.0497 4.8772 1.9298 7.6465v-.007c-.0478.5874-.6494.9616-1.1975.745l-9.7652-3.8596 9.0656 2.4313a7.296 7.296 0 0 0-1.0677-4.5631c-2.9683-4.7678-9.9847-4.5344-12.6297.4201a7.5048 7.5048 0 0 0-.398.8831L5.5546 7.9614c.069-.1017.1417-.198.2144-.2962.1163-.2416.2417-.487.3798-.7268 1.6118-2.7911 4.3102-4.4338 7.1558-4.6973.2108-.0182.4215-.049.6323-.0672z"></path>
|
||||||
|
</svg>
|
||||||
|
));
|
|
@ -0,0 +1,6 @@
|
||||||
|
export * from "./icons/ChevronDownIcon";
|
||||||
|
export * from "./icons/CogIcon";
|
||||||
|
export * from "./icons/LockClosedIcon";
|
||||||
|
export * from "./icons/SkynetLogoIcon";
|
||||||
|
export * from "./icons/ArrowRightIcon";
|
||||||
|
export * from "./icons/InfoIcon";
|
|
@ -0,0 +1,19 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
/**
|
||||||
|
* Size of the icon's bounding box.
|
||||||
|
*/
|
||||||
|
size: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
size: 32,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withIconProps = (IconComponent) => {
|
||||||
|
IconComponent.propTypes = propTypes;
|
||||||
|
IconComponent.defaultProps = defaultProps;
|
||||||
|
|
||||||
|
return IconComponent;
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { PageContainer } from "../PageContainer";
|
||||||
|
|
||||||
|
const NavBarContainer = styled.div.attrs({
|
||||||
|
className: `grid sticky top-0 bg-white`,
|
||||||
|
})``;
|
||||||
|
|
||||||
|
const NavBarBody = styled.nav.attrs({
|
||||||
|
className: "grid h-[80px] font-sans font-light text-sm",
|
||||||
|
})`
|
||||||
|
grid-template-columns: auto max-content 1fr;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const NavBar = (props) => (
|
||||||
|
<NavBarContainer>
|
||||||
|
<PageContainer>
|
||||||
|
<NavBarBody {...props} />
|
||||||
|
</PageContainer>
|
||||||
|
</NavBarContainer>
|
||||||
|
);
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { NavBar, NavBarLink, NavBarSection } from ".";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/NavBar",
|
||||||
|
component: NavBar,
|
||||||
|
subcomponents: {
|
||||||
|
NavBarSection,
|
||||||
|
NavBarLink,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (props) => (
|
||||||
|
<NavBar {...props}>
|
||||||
|
<NavBarSection>
|
||||||
|
<NavBarLink href="/dashboard" active>
|
||||||
|
Dashboard
|
||||||
|
</NavBarLink>
|
||||||
|
<NavBarLink href="/files">Files</NavBarLink>
|
||||||
|
<NavBarLink href="/payments">Payments</NavBarLink>
|
||||||
|
</NavBarSection>
|
||||||
|
</NavBar>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DashboardTopNavigation = Template.bind({});
|
||||||
|
DashboardTopNavigation.args = {};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export const NavBarLink = styled.a.attrs(({ active }) => ({
|
||||||
|
className: `
|
||||||
|
min-w-[168px]
|
||||||
|
flex h-full items-center justify-center
|
||||||
|
border-x border-x-palette-100 border-b-2
|
||||||
|
text-palette-600 transition-colors hover:bg-palette-100/50
|
||||||
|
${active ? "border-b-primary" : "border-b-palette-200/50"}
|
||||||
|
`,
|
||||||
|
}))``;
|
||||||
|
|
||||||
|
NavBarLink.propTypes = {
|
||||||
|
/**
|
||||||
|
* When set to true, an additional indicator will be rendered showing the item as active.
|
||||||
|
*/
|
||||||
|
active: PropTypes.bool,
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export const NavBarSection = styled.div.attrs({ className: "flex items-center" })``;
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./NavBar";
|
||||||
|
export * from "./NavBarSection";
|
||||||
|
export * from "./NavBarLink";
|
|
@ -0,0 +1,13 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export const PageContainer = styled.div.attrs({
|
||||||
|
className: `mx-auto w-page md:w-page-md lg:w-page-lg xl:w-page-xl`,
|
||||||
|
})``;
|
||||||
|
|
||||||
|
PageContainer.propTypes = {
|
||||||
|
/**
|
||||||
|
* Optional `class` attribute.
|
||||||
|
*/
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./PageContainer";
|
|
@ -0,0 +1,33 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const PanelBody = styled.div.attrs({
|
||||||
|
className: "p-6 bg-white rounded",
|
||||||
|
})``;
|
||||||
|
|
||||||
|
const PanelTitle = styled.h6.attrs({
|
||||||
|
className: "uppercase text-xs text-palette-400 h-8 flex items-center",
|
||||||
|
})``;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Besides documented props, it accepts all HMTL attributes a `<div>` element does.
|
||||||
|
*
|
||||||
|
* These additional props will be rendered onto the panel's body element.
|
||||||
|
*/
|
||||||
|
export const Panel = ({ title, ...props }) => (
|
||||||
|
<div>
|
||||||
|
{title && <PanelTitle>{title}</PanelTitle>}
|
||||||
|
<PanelBody {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
Panel.propTypes = {
|
||||||
|
/**
|
||||||
|
* Label of the panel
|
||||||
|
*/
|
||||||
|
title: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
Panel.defaultProps = {
|
||||||
|
title: "",
|
||||||
|
};
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Panel } from "./Panel";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/Panel",
|
||||||
|
component: Panel,
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="inset-0 bg-palette-100 p-6">
|
||||||
|
<div style={{ maxWidth: 800 }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const SampleContent = () => (
|
||||||
|
<>
|
||||||
|
<p>This is the first paragraph</p>
|
||||||
|
<p>This is the second paragraph</p>
|
||||||
|
<p>This is the third paragraph</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Template = (args) => (
|
||||||
|
<Panel {...args}>
|
||||||
|
<SampleContent />
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const RawPanel = Template.bind({});
|
||||||
|
RawPanel.args = {};
|
||||||
|
|
||||||
|
export const TitledPanel = Template.bind({});
|
||||||
|
TitledPanel.args = {
|
||||||
|
title: "Latest activity",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InlinePanelsExample = () => (
|
||||||
|
<div className="grid gap-4 grid-flow-col auto-cols-fr">
|
||||||
|
<Panel title="Upload" className="w-50">
|
||||||
|
<SampleContent />
|
||||||
|
</Panel>
|
||||||
|
<Panel title="Usage" className="w-50">
|
||||||
|
<SampleContent />
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FullWidthPanelsExample = () => (
|
||||||
|
<>
|
||||||
|
<Panel title="Latest activity">
|
||||||
|
<SampleContent />
|
||||||
|
</Panel>
|
||||||
|
<Panel title="Payment history">
|
||||||
|
<SampleContent />
|
||||||
|
</Panel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CustomPanelBackground = Template.bind({});
|
||||||
|
CustomPanelBackground.args = {
|
||||||
|
className: "bg-red-500",
|
||||||
|
title: "Background below should be red",
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./Panel";
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./PopoverMenu";
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { Children, cloneElement, useEffect, useMemo, useRef } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { useClickAway } from "react-use";
|
||||||
|
import styled, { css, keyframes } from "styled-components";
|
||||||
|
|
||||||
|
import { ChevronDownIcon } from "../Icons";
|
||||||
|
import { useCallbacks, useSelectReducer } from "./hooks";
|
||||||
|
import { SelectOption } from "./SelectOption";
|
||||||
|
|
||||||
|
const dropDown = keyframes`
|
||||||
|
0% {
|
||||||
|
transform: scaleY(0);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: scaleY(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaleY(1);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Container = styled.div.attrs({ className: "relative inline-flex" })``;
|
||||||
|
|
||||||
|
const Trigger = styled.button.attrs(({ placeholder }) => ({
|
||||||
|
className: `flex items-center cursor-pointer ${placeholder ? "text-palette-300" : ""}`,
|
||||||
|
}))``;
|
||||||
|
|
||||||
|
const TriggerIcon = styled(ChevronDownIcon).attrs({
|
||||||
|
className: "transition-transform text-primary",
|
||||||
|
})`
|
||||||
|
transform: ${({ open }) => (open ? "rotateX(180deg)" : "none")};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Flyout = styled.ul.attrs(({ open }) => ({
|
||||||
|
className: `absolute top-[20px] right-0
|
||||||
|
p-0 h-0 border rounded bg-white
|
||||||
|
overflow-hidden pointer-events-none
|
||||||
|
shadow-md shadow-palette-200/50
|
||||||
|
${open ? "pointer-events-auto h-auto overflow-visible border-primary" : ""}
|
||||||
|
${open ? "visible" : "invisible"}`,
|
||||||
|
}))`
|
||||||
|
animation: ${({ open }) =>
|
||||||
|
open
|
||||||
|
? css`
|
||||||
|
${dropDown} 0.1s ease-in-out
|
||||||
|
`
|
||||||
|
: "none"};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Select = ({ defaultValue, children, onChange, placeholder }) => {
|
||||||
|
const selectRef = useRef();
|
||||||
|
const options = useMemo(() => Children.toArray(children).filter(({ type }) => type === SelectOption), [children]);
|
||||||
|
const [state, dispatch] = useSelectReducer({ defaultValue, placeholder, options });
|
||||||
|
const { close, toggle, selectOption } = useCallbacks(state, dispatch);
|
||||||
|
|
||||||
|
useClickAway(selectRef, close);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.selectedOptionIndex > -1) {
|
||||||
|
onChange(options[state.selectedOptionIndex].props.value);
|
||||||
|
}
|
||||||
|
}, [onChange, options, state.selectedOptionIndex]);
|
||||||
|
|
||||||
|
const activeOption = options[state.selectedOptionIndex];
|
||||||
|
const activeLabel = activeOption?.props?.label ?? null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container ref={selectRef}>
|
||||||
|
<Trigger placeholder={!activeLabel && placeholder} onClick={toggle}>
|
||||||
|
{activeLabel ?? placeholder} <TriggerIcon open={state.open} />
|
||||||
|
</Trigger>
|
||||||
|
<Flyout role="listbox" open={state.open}>
|
||||||
|
{options.map((item, index) =>
|
||||||
|
cloneElement(item, {
|
||||||
|
...item.props,
|
||||||
|
onClick: () => selectOption(index),
|
||||||
|
selected: state.selectedOptionIndex === index,
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</Flyout>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Select.propTypes = {
|
||||||
|
/**
|
||||||
|
* `<SelectOption>` elements.
|
||||||
|
*/
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
/**
|
||||||
|
* Default value to be selected upon rendering.
|
||||||
|
*/
|
||||||
|
defaultValue: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Callback for every change.
|
||||||
|
*/
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
/**
|
||||||
|
* Placeholder to be displayed when no option is selected.
|
||||||
|
*/
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
};
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Panel } from "../Panel";
|
||||||
|
import { Select, SelectOption } from ".";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/Select",
|
||||||
|
component: Select,
|
||||||
|
subcomponents: {
|
||||||
|
SelectOption,
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Panel style={{ margin: 50, textAlign: "center" }}>
|
||||||
|
<Story />
|
||||||
|
</Panel>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (props) => (
|
||||||
|
<Select {...props}>
|
||||||
|
<SelectOption value="name-asc" label="A-Z" />
|
||||||
|
<SelectOption value="name-desc" label="Z-A" />
|
||||||
|
<SelectOption value="size-desc" label="Biggest size" />
|
||||||
|
<SelectOption value="size-asc" label="Smallest size" />
|
||||||
|
<SelectOption value="date-desc" label="Latest" />
|
||||||
|
<SelectOption value="date-asc" label="Oldest" />
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
Template.args = {};
|
||||||
|
|
||||||
|
export const NoDefaultNoPlaceholder = Template.bind({});
|
||||||
|
NoDefaultNoPlaceholder.args = {
|
||||||
|
onChange: console.info.bind(console, "onChange"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithPlaceholder = Template.bind({});
|
||||||
|
WithPlaceholder.args = {
|
||||||
|
placeholder: "Select...",
|
||||||
|
onChange: console.info.bind(console, "onChange"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithDefautValue = Template.bind({});
|
||||||
|
WithDefautValue.args = {
|
||||||
|
defaultValue: "size-desc",
|
||||||
|
placeholder: "Select...",
|
||||||
|
onChange: console.info.bind(console, "onChange"),
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const Option = styled.li.attrs(({ selected }) => ({
|
||||||
|
className: `m-0 px-4 whitespace-nowrap py-1 px-4 cursor-pointer
|
||||||
|
transition-colors hover:bg-palette-100/50
|
||||||
|
${selected ? "pl-3.5 border-l-2 border-l-primary" : ""}`,
|
||||||
|
}))``;
|
||||||
|
|
||||||
|
export const SelectOption = ({ selected, label, ...props }) => (
|
||||||
|
<Option selected={selected} role="option" {...props}>
|
||||||
|
{label}
|
||||||
|
</Option>
|
||||||
|
);
|
||||||
|
|
||||||
|
SelectOption.propTypes = {
|
||||||
|
/**
|
||||||
|
* Label for the option
|
||||||
|
*/
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
/** Value represented by the option */
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates an option is currently selected. **Controlled by parent `<Select>` component**.
|
||||||
|
*/
|
||||||
|
selected: PropTypes.bool,
|
||||||
|
};
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { useCallback, useReducer } from "react";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
open: false,
|
||||||
|
selectedOptionIndex: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const withDefaultValue = (state, { defaultValue, options, placeholder }) => {
|
||||||
|
let index = -1;
|
||||||
|
|
||||||
|
if (!defaultValue) {
|
||||||
|
if (!placeholder) {
|
||||||
|
// If no default value and no placeholder are provided, select first option.
|
||||||
|
// TODO: might need to look for the first *available* option.
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
index = options.findIndex((option) => {
|
||||||
|
if (!option || !option.props) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return option.props.value === defaultValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedOptionIndex: index,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateReducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "close":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
open: false,
|
||||||
|
};
|
||||||
|
case "open":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
open: true,
|
||||||
|
};
|
||||||
|
case "selectOption":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
open: false,
|
||||||
|
selectedOptionIndex: action.index,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSelectReducer = ({ defaultValue, options, placeholder }) =>
|
||||||
|
useReducer(stateReducer, withDefaultValue(initialState, { defaultValue, options, placeholder }));
|
||||||
|
|
||||||
|
export const useCallbacks = (state, dispatch) => {
|
||||||
|
const close = useCallback(() => {
|
||||||
|
if (state.open) {
|
||||||
|
dispatch({ type: "close" });
|
||||||
|
}
|
||||||
|
}, [dispatch, state.open]);
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
dispatch({ type: state.open ? "close" : "open" });
|
||||||
|
}, [dispatch, state.open]);
|
||||||
|
|
||||||
|
const selectOption = useCallback(
|
||||||
|
(optionIndex) => {
|
||||||
|
if (optionIndex !== state.selectedOptionIndex) {
|
||||||
|
dispatch({ type: "selectOption", index: optionIndex });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, state.selectedOptionIndex]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
close,
|
||||||
|
selectOption,
|
||||||
|
toggle,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./Select";
|
||||||
|
export * from "./SelectOption";
|
|
@ -0,0 +1,40 @@
|
||||||
|
.react-switch-checkbox {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-switch-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 44px;
|
||||||
|
height: 22px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 11px;
|
||||||
|
@apply border-palette-200;
|
||||||
|
border-width: 1px;
|
||||||
|
position: relative;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-switch-label .react-switch-button {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-switch-checkbox:checked + .react-switch-label .react-switch-button {
|
||||||
|
left: calc(100% - 2px);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-switch-label:active .react-switch-button {
|
||||||
|
width: 20px;
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import "./Switch.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary UI component for user interaction
|
||||||
|
*/
|
||||||
|
export const Switch = ({ isOn, handleToggle }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
checked={isOn}
|
||||||
|
onChange={handleToggle}
|
||||||
|
className="react-switch-checkbox"
|
||||||
|
id={`react-switch-new`}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label className={"react-switch-label"} htmlFor={`react-switch-new`}>
|
||||||
|
<span className={`react-switch-button ${isOn ? "bg-primary" : "bg-palette-200"}`} />
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Switch.propTypes = {
|
||||||
|
/**
|
||||||
|
* Switch's current value
|
||||||
|
*/
|
||||||
|
isOn: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Function to execute on change
|
||||||
|
*/
|
||||||
|
handleToggle: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
Switch.defaultProps = {
|
||||||
|
isOn: false,
|
||||||
|
};
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Switch } from "./Switch";
|
||||||
|
|
||||||
|
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/Switch",
|
||||||
|
component: Switch,
|
||||||
|
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||||
|
};
|
||||||
|
|
||||||
|
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||||
|
const Template = (args) => <Switch {...args} />;
|
||||||
|
|
||||||
|
export const SwitchTrue = Template.bind({});
|
||||||
|
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
SwitchTrue.args = {
|
||||||
|
isOn: true,
|
||||||
|
};
|
||||||
|
export const SwitchFalse = Template.bind({});
|
||||||
|
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
SwitchFalse.args = {
|
||||||
|
isOn: false,
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./Switch";
|
|
@ -0,0 +1,20 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const Container = styled.div.attrs({
|
||||||
|
className: "p-1 max-w-full overflow-x-auto",
|
||||||
|
})``;
|
||||||
|
|
||||||
|
const StyledTable = styled.table.attrs({
|
||||||
|
className: "table-auto w-full border-separate",
|
||||||
|
})`
|
||||||
|
border-spacing: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts all HMTL attributes a `<table>` element does.
|
||||||
|
*/
|
||||||
|
export const Table = (props) => (
|
||||||
|
<Container>
|
||||||
|
<StyledTable {...props} />
|
||||||
|
</Container>
|
||||||
|
);
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { CogIcon } from "../Icons";
|
||||||
|
|
||||||
|
import { IconButton } from "../IconButton";
|
||||||
|
import { Table, TableBody, TableHead, TableCell, TableRow, TableHeadCell } from "./";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/Table",
|
||||||
|
component: Table,
|
||||||
|
subcomponents: {
|
||||||
|
TableBody,
|
||||||
|
TableHead,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
TableHeadCell,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DATA = [
|
||||||
|
{
|
||||||
|
name: "At_vereo_eos_censes",
|
||||||
|
type: ".mp4",
|
||||||
|
size: "2.45 MB",
|
||||||
|
uploaded: "a few seconds ago",
|
||||||
|
skylink: "_HyFqH632Rmy99c93idTtBVXeRDgaDAAWg6Bmm5P1izriu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Miriam Klein IV",
|
||||||
|
type: ".pdf",
|
||||||
|
size: "7.52 MB",
|
||||||
|
uploaded: "01/04/2021; 17:11",
|
||||||
|
skylink: "_izriuHyFqH632Rmy99c93idTtBVXeRDgaDAAWg6Bmm5P1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tmp/QmWR6eVDVkwhAYq7X99w4xT9KNKBzwK39Fj1PDmr4ZnzMm/QmWR6eVDVkwhAYq7X99w4xT9KNKBzwK39Fj1PDmr4ZnzMm",
|
||||||
|
type: ".doc",
|
||||||
|
size: "8.15 MB",
|
||||||
|
uploaded: "10/26/2020; 7:21",
|
||||||
|
skylink: "_VXeRDgaDAAWg6Bmm5P1izriuHyFqH632Rmy99c93idTtB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Perm_London",
|
||||||
|
type: ".avi",
|
||||||
|
size: "225.6 MB",
|
||||||
|
uploaded: "09/12/2020; 19:28",
|
||||||
|
skylink: "_eRDgaDAAWg6Bmm5P1izriuHyFqH632Rmy99c93idTtBVX",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Santa_Clara",
|
||||||
|
type: ".pdf",
|
||||||
|
size: "7.52 MB",
|
||||||
|
uploaded: "09/12/2020; 19:23",
|
||||||
|
skylink: "_AWg6Bmm5P1izriuHyFqH632Rmy99c93idTtBVXeRDgaDA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Marysa_Labrone",
|
||||||
|
type: ".doc",
|
||||||
|
size: "8.15 MB",
|
||||||
|
uploaded: "09/12/2020; 19:21",
|
||||||
|
skylink: "_P1izriuHyFqH632Rmy99c93idTtBVXeRDgaDAAWg6Bmm5",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Template = (args) => (
|
||||||
|
<Table {...args}>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow noHoverEffect>
|
||||||
|
<TableHeadCell>Name</TableHeadCell>
|
||||||
|
<TableHeadCell>Type</TableHeadCell>
|
||||||
|
<TableHeadCell align="right">Size</TableHeadCell>
|
||||||
|
<TableHeadCell>Uploaded</TableHeadCell>
|
||||||
|
<TableHeadCell>Skylink</TableHeadCell>
|
||||||
|
<TableHeadCell>Activity</TableHeadCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{DATA.map(({ name, type, size, uploaded, skylink }) => (
|
||||||
|
<TableRow key={skylink}>
|
||||||
|
<TableCell maxWidth="200px">{name}</TableCell>
|
||||||
|
<TableCell>{type}</TableCell>
|
||||||
|
<TableCell align="right">{size}</TableCell>
|
||||||
|
<TableCell>{uploaded}</TableCell>
|
||||||
|
<TableCell>{skylink}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<IconButton icon={<CogIcon width={24} color="#0d0d0d" />}></IconButton>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const RegularTable = Template.bind({});
|
||||||
|
RegularTable.args = {};
|
|
@ -0,0 +1,6 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts all HMTL attributes a `<tbody>` element does.
|
||||||
|
*/
|
||||||
|
export const TableBody = styled.tbody``;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts all HMTL attributes a `<td>` element does.
|
||||||
|
*/
|
||||||
|
export const TableCell = styled.td.attrs({
|
||||||
|
className: `px-6 py-4 h-tableRow truncate
|
||||||
|
text-palette-600 even:text-palette-400
|
||||||
|
first:rounded-l-sm last:rounded-r-sm`,
|
||||||
|
})`
|
||||||
|
text-align: ${({ align }) => align ?? "left"};
|
||||||
|
max-width: ${({ maxWidth }) => maxWidth ?? "none"};
|
||||||
|
`;
|
|
@ -0,0 +1,6 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts all HMTL attributes a `<thead>` element does.
|
||||||
|
*/
|
||||||
|
export const TableHead = styled.thead``;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts all HMTL attributes a `<th>` element does.
|
||||||
|
*/
|
||||||
|
export const TableHeadCell = styled.th.attrs({
|
||||||
|
className: `px-6 py-2.5 truncate h-tableRow
|
||||||
|
text-palette-600 font-sans font-light text-xs
|
||||||
|
first:rounded-l-sm last:rounded-r-sm`,
|
||||||
|
})`
|
||||||
|
text-align: ${({ align }) => align ?? "left"};
|
||||||
|
max-width: ${({ maxWidth }) => maxWidth ?? "none"};
|
||||||
|
`;
|
|
@ -0,0 +1,20 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Besides documented props, it accepts all HMTL attributes a `<tr>` element does.
|
||||||
|
*/
|
||||||
|
export const TableRow = styled.tr.attrs(({ noHoverEffect }) => ({
|
||||||
|
className: `bg-palette-100/50 odd:bg-white ${noHoverEffect ? "" : "hover:bg-palette-200/20"}`,
|
||||||
|
}))``;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows disabling `hover` effect on a row. Useful for `<thead>` row.
|
||||||
|
*/
|
||||||
|
TableRow.propTypes = {
|
||||||
|
noHoverEffect: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
TableRow.defaultProps = {
|
||||||
|
noHoverEffect: false,
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
export * from "./Table";
|
||||||
|
export * from "./TableHead";
|
||||||
|
export * from "./TableHeadCell";
|
||||||
|
export * from "./TableBody";
|
||||||
|
export * from "./TableRow";
|
||||||
|
export * from "./TableCell";
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const Wrapper = styled.div.attrs({
|
||||||
|
className: "absolute left-0 bottom-0 w-full h-0.5 bg-palette-200",
|
||||||
|
})``;
|
||||||
|
|
||||||
|
const Indicator = styled.div.attrs({
|
||||||
|
className: "absolute h-0.5 bottom-0 bg-primary duration-200 ease-in-out",
|
||||||
|
})`
|
||||||
|
will-change: left, width;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ActiveTabIndicator = ({ tabRef }) => {
|
||||||
|
const [position, setPosition] = useState(0);
|
||||||
|
const [width, setWidth] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tabRef?.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { offsetLeft, offsetWidth } = tabRef.current;
|
||||||
|
setPosition(offsetLeft);
|
||||||
|
setWidth(offsetWidth);
|
||||||
|
}, [tabRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<Indicator style={{ left: position, width: `${width}px` }} />
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ActiveTabIndicator.propTypes = {
|
||||||
|
tabRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]),
|
||||||
|
};
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export const StyledTab = styled.button.attrs(({ active, variant }) => ({
|
||||||
|
className: `m-0 px-2 pb-2
|
||||||
|
text-tab text-left font-sans
|
||||||
|
transition-colors hover:text-palette-500
|
||||||
|
${active ? "font-semibold text-palette-600" : "font-light text-palette-300"}
|
||||||
|
${variant === "regular" ? "sm:min-w-[180px]" : ""}`,
|
||||||
|
}))``;
|
||||||
|
|
||||||
|
export const Tab = forwardRef(({ active, title, id, variant, ...props }, ref) => (
|
||||||
|
<StyledTab
|
||||||
|
ref={ref}
|
||||||
|
role="tab"
|
||||||
|
type="button"
|
||||||
|
ariaSelected={`${active ? "true" : "false"}`}
|
||||||
|
ariaControls={`tabpanel-${id}`}
|
||||||
|
id={`tab-${id}`}
|
||||||
|
active={active}
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</StyledTab>
|
||||||
|
));
|
||||||
|
|
||||||
|
Tab.displayName = "Tab";
|
||||||
|
|
||||||
|
Tab.propTypes = {
|
||||||
|
/**
|
||||||
|
* Used by `Tabs` component to control the `active` property, and also
|
||||||
|
* in the HTML markup for accessibility purposes.
|
||||||
|
*
|
||||||
|
* Should be set to the same value as related `TabPanel`'s `tabId` prop.
|
||||||
|
*/
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* Used as a label.
|
||||||
|
*/
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* Controlled by `Tabs` component.
|
||||||
|
*/
|
||||||
|
active: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Controlled by `Tabs` component.
|
||||||
|
*/
|
||||||
|
variant: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
Tab.defaultProps = {
|
||||||
|
variant: "regular",
|
||||||
|
};
|
|
@ -0,0 +1,31 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Besides documented props, it accepts all HMTL attributes a `<div>` element does.
|
||||||
|
*/
|
||||||
|
export const TabPanel = ({ children, active, tabId, ...props }) => {
|
||||||
|
if (!active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div role="tabpanel" id={`tabpanel-${tabId}`} aria-labelledby={`tab-${tabId}`} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TabPanel.propTypes = {
|
||||||
|
/**
|
||||||
|
* Used by `Tabs` component to control the `active` property, and also
|
||||||
|
* in the HTML markup for accessibility purposes.
|
||||||
|
*
|
||||||
|
* Should be set to the same value as related `Tab`'s `id` prop.
|
||||||
|
*/
|
||||||
|
tabId: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.node,
|
||||||
|
/**
|
||||||
|
* Controlled by `Tabs` component.
|
||||||
|
*/
|
||||||
|
active: PropTypes.bool,
|
||||||
|
};
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { cloneElement, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { ActiveTabIndicator } from "./ActiveTabIndicator";
|
||||||
|
import { usePrefixedTabIds, useTabsChildren } from "./hooks";
|
||||||
|
|
||||||
|
const Container = styled.div.attrs({
|
||||||
|
className: "tabs-container",
|
||||||
|
})``;
|
||||||
|
|
||||||
|
const Header = styled.div.attrs({
|
||||||
|
className: "relative flex justify-start overflow-hidden",
|
||||||
|
})``;
|
||||||
|
|
||||||
|
const TabList = styled.div.attrs(({ variant }) => ({
|
||||||
|
role: "tablist",
|
||||||
|
className: `relative inline-grid grid-flow-col auto-cols-fr
|
||||||
|
${variant === "regular" ? "w-full sm:w-auto" : "w-full"}`,
|
||||||
|
}))``;
|
||||||
|
|
||||||
|
const Divider = styled.div.attrs({
|
||||||
|
"aria-hidden": "true",
|
||||||
|
className: "absolute bottom-0 w-screen border-b border-b-palette-200",
|
||||||
|
})`
|
||||||
|
right: calc(-100vw - 2px);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Body = styled.div``;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Besides documented props, it accepts all HMTL attributes a `<div>` element does.
|
||||||
|
*/
|
||||||
|
export const Tabs = ({ defaultTab, children, variant }) => {
|
||||||
|
const getTabId = usePrefixedTabIds();
|
||||||
|
const { tabs, panels, tabsRefs } = useTabsChildren(children, getTabId);
|
||||||
|
const defaultTabId = useMemo(() => getTabId(defaultTab || tabs[0].props.id), [getTabId, defaultTab, tabs]);
|
||||||
|
const [activeTabId, setActiveTabId] = useState(defaultTabId);
|
||||||
|
const [activeTabRef, setActiveTabRef] = useState(tabsRefs[activeTabId]);
|
||||||
|
const isActive = (id) => id === activeTabId;
|
||||||
|
const onTabChange = useCallback(
|
||||||
|
(id) => {
|
||||||
|
setActiveTabId(id);
|
||||||
|
},
|
||||||
|
[setActiveTabId]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Refresh active tab indicator whenever active tab changes.
|
||||||
|
setActiveTabRef(tabsRefs[activeTabId]);
|
||||||
|
}, [setActiveTabRef, tabsRefs, activeTabId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Header>
|
||||||
|
<TabList variant={variant}>
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const tabId = getTabId(tab.props.id);
|
||||||
|
|
||||||
|
return cloneElement(tab, {
|
||||||
|
ref: tabsRefs[tabId],
|
||||||
|
id: tabId,
|
||||||
|
variant,
|
||||||
|
active: isActive(tabId),
|
||||||
|
onClick: () => onTabChange(tabId),
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
<Divider />
|
||||||
|
<ActiveTabIndicator tabRef={activeTabRef} />
|
||||||
|
</TabList>
|
||||||
|
</Header>
|
||||||
|
<Body>
|
||||||
|
{panels.map((panel) => {
|
||||||
|
const tabId = getTabId(panel.props.tabId);
|
||||||
|
|
||||||
|
return cloneElement(panel, {
|
||||||
|
...panel.props,
|
||||||
|
tabId,
|
||||||
|
active: isActive(tabId),
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</Body>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Tabs.propTypes = {
|
||||||
|
/**
|
||||||
|
* Should include `<Tab />` and `<TabPanel />` components.
|
||||||
|
*/
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
/**
|
||||||
|
* ID of the `<Tab />` which should be open by default
|
||||||
|
*/
|
||||||
|
defaultTab: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* `regular` (default) will make the tabs only take as much space as they need
|
||||||
|
*
|
||||||
|
* `fill` will make the tabs spread throughout the available width
|
||||||
|
*/
|
||||||
|
variant: PropTypes.oneOf(["regular", "fill"]),
|
||||||
|
};
|
||||||
|
|
||||||
|
Tabs.defaultProps = {
|
||||||
|
variant: "regular",
|
||||||
|
};
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Tab, TabPanel, Tabs } from "./";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/Tabs",
|
||||||
|
component: Tabs,
|
||||||
|
subcomponents: { Tab, TabPanel },
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (props) => (
|
||||||
|
<>
|
||||||
|
<Tabs {...props}>
|
||||||
|
<Tab id="uploads" title="Uploads" />
|
||||||
|
<Tab id="downloads" title="Downloads" />
|
||||||
|
<TabPanel tabId="uploads">
|
||||||
|
<ul>
|
||||||
|
<li>Upload #1</li>
|
||||||
|
<li>Upload #2</li>
|
||||||
|
<li>Upload #3</li>
|
||||||
|
<li>Upload #4</li>
|
||||||
|
</ul>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel tabId="downloads">
|
||||||
|
<ul>
|
||||||
|
<li>Download #1</li>
|
||||||
|
<li>Download #2</li>
|
||||||
|
<li>Download #3</li>
|
||||||
|
<li>Download #4</li>
|
||||||
|
</ul>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RegularTabs = Template.bind({});
|
||||||
|
|
||||||
|
const FillingTabs = Template.bind({});
|
||||||
|
FillingTabs.args = {
|
||||||
|
variant: "fill",
|
||||||
|
};
|
||||||
|
|
||||||
|
const FillingTabsInNarrowContainer = Template.bind({});
|
||||||
|
FillingTabsInNarrowContainer.args = {
|
||||||
|
variant: "fill",
|
||||||
|
defaultTab: "downloads",
|
||||||
|
};
|
||||||
|
FillingTabsInNarrowContainer.decorators = [
|
||||||
|
(Story) => (
|
||||||
|
<div style={{ width: 360, background: "#fafafa", padding: 10, border: "1px solid #eee" }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const MultipleTabsComponents = Template.bind({});
|
||||||
|
MultipleTabsComponents.args = {
|
||||||
|
variant: "fill",
|
||||||
|
};
|
||||||
|
MultipleTabsComponents.decorators = [
|
||||||
|
(Story) => (
|
||||||
|
<div style={{ width: 360, background: "#fafafa", padding: 10, border: "1px solid #eee" }}>
|
||||||
|
<Story />
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
export { RegularTabs, FillingTabs, FillingTabsInNarrowContainer, MultipleTabsComponents };
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Children, createRef, useCallback, useMemo } from "react";
|
||||||
|
|
||||||
|
import { Tab } from "./Tab";
|
||||||
|
import { TabPanel } from "./TabPanel";
|
||||||
|
|
||||||
|
export const usePrefixedTabIds = () => {
|
||||||
|
const seed = useMemo(() => Math.random().toString().split(".")[1], []);
|
||||||
|
|
||||||
|
return useCallback((id) => `${seed}-${id}`, [seed]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTabsChildren = (children, prefixId) => {
|
||||||
|
const childrenArray = useMemo(() => Children.toArray(children), [children]);
|
||||||
|
const tabs = useMemo(() => childrenArray.filter(({ type }) => type === Tab), [childrenArray]);
|
||||||
|
const panels = useMemo(() => childrenArray.filter(({ type }) => type === TabPanel), [childrenArray]);
|
||||||
|
const tabsRefs = useMemo(
|
||||||
|
() =>
|
||||||
|
tabs.reduce(
|
||||||
|
(refs, tab) => ({
|
||||||
|
...refs,
|
||||||
|
[prefixId(tab.props.id)]: createRef(),
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
[tabs, prefixId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tabs,
|
||||||
|
panels,
|
||||||
|
tabsRefs,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./Tab";
|
||||||
|
export * from "./Tabs";
|
||||||
|
export * from "./TabPanel";
|
|
@ -0,0 +1,31 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary UI component for user interaction
|
||||||
|
*/
|
||||||
|
export const TextIndicator = ({ variant }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex justify-center items-center w-textIndicator h-textIndicator text-textIndicator font-sans uppercase tracking-wide text-button bg-opacity-10 ${
|
||||||
|
variant === "success"
|
||||||
|
? "text-primary bg-primary"
|
||||||
|
: variant === "next"
|
||||||
|
? "text-next bg-next"
|
||||||
|
: "text-error bg-error"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{variant === "success" ? "success" : variant === "next" ? "next" : "error"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TextIndicator.propTypes = {
|
||||||
|
/**
|
||||||
|
* Variant of text indicator
|
||||||
|
*/
|
||||||
|
variant: PropTypes.oneOf(["success", "next", "error"]),
|
||||||
|
};
|
||||||
|
|
||||||
|
TextIndicator.defaultProps = {
|
||||||
|
variant: "success",
|
||||||
|
};
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { TextIndicator } from "./TextIndicator";
|
||||||
|
|
||||||
|
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/TextIndicator",
|
||||||
|
component: TextIndicator,
|
||||||
|
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||||
|
};
|
||||||
|
|
||||||
|
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||||
|
const Template = (args) => <TextIndicator {...args} />;
|
||||||
|
|
||||||
|
export const Success = Template.bind({});
|
||||||
|
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
Success.args = {
|
||||||
|
variant: "success",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Next = Template.bind({});
|
||||||
|
Next.args = {
|
||||||
|
variant: "next",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Error = Template.bind({});
|
||||||
|
Error.args = {
|
||||||
|
variant: "error",
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./TextIndicator";
|
|
@ -0,0 +1,30 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary UI component for user interaction
|
||||||
|
*/
|
||||||
|
export const TextInputBasic = ({ label, placeholder }) => {
|
||||||
|
return (
|
||||||
|
<div className={""}>
|
||||||
|
<p className={"font-sans uppercase text-palette-300 text-inputLabel mb-textInputLabelBottom"}>{label}</p>
|
||||||
|
<input
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={
|
||||||
|
"w-full bg-palette-100 h-textInput px-textInputBasicX focus:outline-none bg-transparent " +
|
||||||
|
"placeholder-palette-400 text-content tracking-inputPlaceholder text-textInput"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TextInputBasic.propTypes = {
|
||||||
|
/**
|
||||||
|
* Icon to place in text input
|
||||||
|
*/
|
||||||
|
label: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Input placeholder
|
||||||
|
*/
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { TextInputBasic } from "./TextInputBasic";
|
||||||
|
|
||||||
|
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/TextInputBasic",
|
||||||
|
component: TextInputBasic,
|
||||||
|
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||||
|
};
|
||||||
|
|
||||||
|
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||||
|
const Template = (args) => <TextInputBasic {...args} />;
|
||||||
|
|
||||||
|
export const Input = Template.bind({});
|
||||||
|
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
Input.args = {
|
||||||
|
label: "Display Name",
|
||||||
|
placeholder: "Your Name",
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./TextInputBasic";
|
|
@ -0,0 +1,35 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary UI component for user interaction
|
||||||
|
*/
|
||||||
|
export const TextInputIcon = ({ icon, position, placeholder }) => {
|
||||||
|
return (
|
||||||
|
<div className={"flex flex-row items-center px-textInputIcon h-textInput rounded-full bg-palette-100"}>
|
||||||
|
{position === "left" ? <div className={"w-buttonIconLg h-buttonIconLg"}>{icon}</div> : null}
|
||||||
|
<input
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={
|
||||||
|
"w-full focus:outline-none mx-textInputHorizontal rounded-full bg-transparent " +
|
||||||
|
"placeholder-palette-400 text-content tracking-inputPlaceholder text-textInput"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{position === "right" ? <div className={"w-buttonIconLg h-buttonIconLg"}>{icon}</div> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TextInputIcon.propTypes = {
|
||||||
|
/**
|
||||||
|
* Icon to place in text input
|
||||||
|
*/
|
||||||
|
icon: PropTypes.element,
|
||||||
|
/**
|
||||||
|
* Side to place icon
|
||||||
|
*/
|
||||||
|
position: PropTypes.oneOf(["left", "right"]),
|
||||||
|
/**
|
||||||
|
* Input placeholder
|
||||||
|
*/
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
};
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { TextInputIcon } from "./TextInputIcon";
|
||||||
|
import { CogIcon } from "../Icons";
|
||||||
|
|
||||||
|
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||||
|
export default {
|
||||||
|
title: "SkynetLibrary/TextInputIcon",
|
||||||
|
component: TextInputIcon,
|
||||||
|
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||||
|
};
|
||||||
|
|
||||||
|
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||||
|
const Template = (args) => <TextInputIcon {...args} />;
|
||||||
|
|
||||||
|
export const IconLeft = Template.bind({});
|
||||||
|
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
IconLeft.args = {
|
||||||
|
icon: <CogIcon />,
|
||||||
|
position: "left",
|
||||||
|
placeholder: "Search",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IconRight = Template.bind({});
|
||||||
|
IconRight.args = {
|
||||||
|
icon: <CogIcon />,
|
||||||
|
position: "right",
|
||||||
|
placeholder: "Search",
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./TextInputIcon";
|
|
@ -1,3 +1,18 @@
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
@apply font-content bg-palette-200 bg-opacity-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ module.exports = {
|
||||||
content: ["Source\\ Sans\\ Pro", ...defaultTheme.fontFamily.sans],
|
content: ["Source\\ Sans\\ Pro", ...defaultTheme.fontFamily.sans],
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
|
body: ["21px", { lineHeight: "1.58" }],
|
||||||
tab: ["18px", "28px"],
|
tab: ["18px", "28px"],
|
||||||
},
|
},
|
||||||
backgroundColor: ["disabled"],
|
backgroundColor: ["disabled"],
|
||||||
|
@ -53,6 +54,9 @@ module.exports = {
|
||||||
"page-lg": "896px",
|
"page-lg": "896px",
|
||||||
"page-xl": "1312px",
|
"page-xl": "1312px",
|
||||||
},
|
},
|
||||||
|
minWidth: {
|
||||||
|
button: "112px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue