Skip to content

Commit

Permalink
Add new floating panels component
Browse files Browse the repository at this point in the history
  • Loading branch information
abottega committed Jan 8, 2025
1 parent 505c170 commit 26ae339
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 0 deletions.
56 changes: 56 additions & 0 deletions lib/components/FloatingPanels/FloatingPanels.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Story, Canvas, Controls, Meta } from "@storybook/addon-docs";
import FloatingPanels from ".";
import * as stories from "./FloatingPanels.stories";

<Meta of={stories} />

# Floating Panels

The Floating Panels Component is a flexible React component that creates a set of collapsible panels floating on the screen. It's ideal for displaying information or controls that need to be easily accessible but not always visible.

## Features

- Customizable position on the screen
- Expandable/collapsible panels
- Configurable icons and titles for each panel
- Ability to include any content within panels

## Usage

```js run
const panels = [
{
id: "legend",
iconName: "map",
title: "Legend",
defaultExpanded: true,
children: <div>Legend content goes here</div>
},
{
id: "view-options",
iconName: "eye",
title: "View Options",
defaultExpanded: true,
children: <div>View options content goes here</div>
},
{
id: "person-details",
iconName: "user",
title: "Person Details",
children: <div>Person details content goes here</div>
},
{
id: "properties",
iconName: "gear",
title: "Properties",
children: <div>Properties content goes here</div>
}
// Add more panels as needed...
];
```

<Canvas of={stories.defaultFloatingPanels} />

## Properties

<Controls component={FloatingPanels} />
59 changes: 59 additions & 0 deletions lib/components/FloatingPanels/FloatingPanels.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from "react";
import FloatingPanels from ".";
import Box from "../Box";
import { far } from "@fortawesome/free-regular-svg-icons";
import { library } from "@fortawesome/fontawesome-svg-core";
import TextInput from "../TextInput";
import Toggle from "../Toggle";
import Spacer from "../Spacer";

library.add(far);

export default {
title: "Components/FloatingPanels",
decorators: [(storyFn) => <Box minHeight="450px">{storyFn()}</Box>],
component: FloatingPanels
};
const Properties = () => {
return (
<Spacer mb="r">
<Toggle id="toggle1" label="Group items" small />
<Toggle id="toggle2" label="Show teams" small />
<TextInput
id="textInput1"
key="textInput1"
type="text"
label="Full name"
placeholder="E.g. John Smith"
my="20px"
/>
</Spacer>
);
};
const panels = [
{
id: "view-options",
iconName: "eye",
title: "View Options",
defaultExpanded: true,
children: <div>View options content goes here</div>
},
{
id: "properties",
iconName: "sun",
title: "Properties",
children: <Properties />
},
{
id: "person-details",
iconName: "user",
title: "Person Details",
children: <div>Person details content goes here</div>
}
];

export const defaultFloatingPanels = () => {
return <FloatingPanels panels={panels} position={{ right: 20, top: 20 }} />;
};

defaultFloatingPanels.storyName = "Default Floating Panels";
90 changes: 90 additions & 0 deletions lib/components/FloatingPanels/FloatingPanels.styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import styled from "styled-components";
import { themeGet } from "@styled-system/theme-get";

export const Container = styled.div`
position: fixed;
display: flex;
flex-direction: column;
gap: 8px;
width: 300px;
${({ position }) =>
Object.entries(position)
.filter(([, value]) => value !== undefined)
.map(
([key, value]) =>
`${key}: ${typeof value === "number" ? `${value}px` : value};`
)
.join("\n")}
`;

export const PanelWrapper = styled.div`
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
padding: 0 12px;
`;

export const PanelHeader = styled.button`
font-family: ${themeGet("fonts.main")};
color: ${themeGet("colors.greyDarkest")};
width: 100%;
border: none;
appearance: none;
background-color: white;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
padding: ${({ $isExpanded }) => ($isExpanded ? "12px 0 0 0" : "12px 0")};
cursor: pointer;
user-select: none;
transition: padding 0.3s ease-in-out;
&:focus {
outline: none;
}
`;

export const HeaderContent = styled.div`
display: flex;
align-items: center;
gap: 12px;
`;

export const Title = styled.span`
font-size: 14px;
font-weight: 500;
`;

export const IconWrapper = styled.div`
background-color: #3b82f6;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
`;

export const ChevronWrapper = styled(IconWrapper)`
transition: background-color 0.3s ease-in-out;
background-color: ${({ isHovered }) =>
isHovered ? themeGet("colors.greyLighter") : "white"};
`;

export const PanelContent = styled.div`
padding: ${({ $isExpanded }) => ($isExpanded ? "16px 0 12px 0" : "0")};
height: ${({ $isExpanded }) => ($isExpanded ? "auto" : "0")};
overflow-y: ${({ $isExpanded }) =>
$isExpanded
? "auto"
: "hidden"}; // note: this is showing briefly on transition, how do I hide this? Animation delay on this one maybe?
max-height: ${({ $isExpanded }) =>
$isExpanded
? "500px"
: "0"}; // Make max width a split of a set height passed in props divided by amount of panels?
opacity: ${({ $isExpanded }) => ($isExpanded ? "1" : "0")};
transition: maxHeight 0.3s ease-in-out, opacity 0.3s ease-in-out,
padding 0.3s ease-in-out;
`;
55 changes: 55 additions & 0 deletions lib/components/FloatingPanels/Panel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useState } from "react";
import PropTypes from "prop-types";

import Icon from "../Icon";
import {
PanelWrapper,
PanelHeader,
HeaderContent,
Title,
IconWrapper,
ChevronWrapper,
PanelContent
} from "./FloatingPanels.styles";

const Panel = ({ iconName, title, children, defaultExpanded = false }) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const arrowIcon = isExpanded ? "chevron-up" : "chevron-down";
const [isHovered, setIsHovered] = useState(false);
return (
<PanelWrapper>
<PanelHeader
onClick={() => setIsExpanded(!isExpanded)}
$isExpanded={isExpanded}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onFocus={() => setIsHovered(true)}
onBlur={() => setIsHovered(false)}
>
<HeaderContent>
<IconWrapper>
<Icon size="xs" color="white" icon={["far", iconName]} />
</IconWrapper>
<Title>{title}</Title>
</HeaderContent>
<ChevronWrapper isHovered={isHovered}>
<Icon size="sm" icon={["fas", arrowIcon]} color="greyDarker" />
</ChevronWrapper>
</PanelHeader>
<PanelContent $isExpanded={isExpanded}>{children}</PanelContent>
</PanelWrapper>
);
};

Panel.propTypes = {
iconName: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
defaultExpanded: PropTypes.bool
};

Panel.defaultProps = {
defaultExpanded: false
};

export default Panel;
38 changes: 38 additions & 0 deletions lib/components/FloatingPanels/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react";
import { Container } from "./FloatingPanels.styles";
import Panel from "./Panel";
import PropTypes from "prop-types";

const FloatingPanels = ({ panels, position = { right: 20, top: 20 } }) => {
return (
<Container position={position}>
{panels.map((panel) => (
<Panel key={panel.id} {...panel} />
))}
</Container>
);
};

FloatingPanels.propTypes = {
panels: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
iconName: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
defaultExpanded: PropTypes.bool
})
).isRequired,
position: PropTypes.shape({
top: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
right: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
bottom: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
left: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
})
};

FloatingPanels.defaultProps = {
position: { right: 20, top: 20 }
};

export default FloatingPanels;

0 comments on commit 26ae339

Please sign in to comment.