Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@ import React from "react";

export interface HeaderProps {
logo?: React.ReactNode;
elements?: React.ReactNode[];
elements?: React.ReactElement[];
}

const Header: FC<HeaderProps> = ({ logo, elements = [] }) => {
return (
<header className="header" role="banner">
<div className="header">
<div className="header__container">
{!!logo && <div className="header__logo">{logo}</div>}

{elements.length > 0 && (
<div className="header__elements">{elements}</div>
<div className="header__elements">
{elements.map((node, idx) => (
<div key={node.key ?? idx}>{node}</div>
))}
</div>
)}
</div>
</header>
</div>
);
};

Expand Down
116 changes: 116 additions & 0 deletions src/Navigation/Navigation.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { Meta, StoryObj } from "@storybook/react-vite";

import Navigation from ".";
import { Placeholder } from "../Placeholder";
import Header from "../Header";
import ColorSchemeToggle from "../ColorSchemeToggle";
import DetSysIcon from "../DetSysIcon";

const meta = {
title: "Molecules/Navigation",
component: Navigation,
argTypes: {
initialState: {
options: ["open", "closed"],
},
elements: { type: "function" },
},
} satisfies Meta<typeof Navigation>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
initialState: undefined,
elements: [
<Placeholder key="primary" height="100%" label="Primary Element" />,
<Placeholder key="nav" height="100%" label="Navigation Link" />,
],
},
render: (props) => (
<div
style={{
height: "300px",
width: "500px",
border: "1px solid black",
display: "flex",
justifyContent: "end",
}}
>
<Navigation {...props} />
</div>
),
};

export const InHeader: Story = {
args: {
initialState: undefined,
elements: [
<Placeholder key="lorem" height="100%" label="Lorem" />,
<Placeholder key="ipsum" height="100%" label="Ipsum" />,
],
},
render: (props) => (
<Header
logo={<DetSysIcon size="base" />}
elements={[
<ColorSchemeToggle key="color-scheme-toggle" />,
<Navigation key="nav" {...props} />,
]}
/>
),
};

export const DefaultOpen: Story = {
args: {
initialState: "open",
elements: [
<Placeholder key="lorem" height="100%" label="Lorem" />,
<Placeholder key="ipsum" height="100%" label="Ipsum" />,
],
},
render: (props) => (
<>
<div
style={{
height: "300px",
width: "500px",
border: "1px solid black",
display: "flex",
justifyContent: "end",
}}
>
<Navigation {...props} />
</div>
</>
),
};

export const InHeaderOpen: Story = {
args: {
initialState: "open",
elements: [
<a key="docs" href="#">
Docs
</a>,
<a key="flakes" href="#">
Flakes
</a>,
<a key="orgs" href="#">
Orgs
</a>,
<Placeholder key="lorem" height="100%" label="Lorem" />,
<Placeholder key="ipsum" height="100%" label="Ipsum" />,
],
},
render: (props) => (
<Header
logo={<DetSysIcon size="base" />}
elements={[
<ColorSchemeToggle key="color-scheme-toggle" />,
<Navigation key="nav" {...props} />,
]}
/>
),
};
75 changes: 75 additions & 0 deletions src/Navigation/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
@use "sass:map";

@use "../sass/mixins";
@use "../sass/tokens";
@use "../sass/functions";

@media screen and (width >= 1024px) {
.navigation {
height: 24px; // Matches the svg-icon height used by the expander in closed mode
padding: 4px; // Matches the svg-icon height used by the expander in closed mode
.navigation__expand {
display: none;
}

.navigation__elements {
height: 24px;
display: flex;
flex-direction: row;
align-items: center;
gap: 1em;
}
}
}

@media screen and (width < 1024px) {
.navigation {
position: relative;

.navigation__expand {
@include mixins.svg-button;
}

.navigation__elements {
display: flex;
flex-direction: column;
gap: map.get(tokens.$spacing, sm);

max-width: 100vw;

position: absolute;
transform-origin: 100% 0;
z-index: 10;
right: 0;
top: calc(100% + #{map.get(tokens.$spacing, xs)});

@include mixins.pad(base);
border-radius: map.get(tokens.$border-radius, base);

background-color: map.get(tokens.$brand, very-black);
$offset: map.get(tokens.$spacing, xs);
border-style: solid;
border-width: 1px;
border-color: map.get(tokens.$brand, black);
@include mixins.light-mode {
border: none;
background-color: map.get(tokens.$brand, white);
box-shadow:
$offset $offset $offset 0px map.get(tokens.$brand, light),
-$offset $offset $offset 0px map.get(tokens.$brand, light);
}
}

.navigation__element {
width: min(320px, 100vw);

@media screen and (width < 650px) {
width: 268px;
}
}
}

.navigation--closed .navigation__elements {
display: none;
}
}
57 changes: 57 additions & 0 deletions src/Navigation/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useId, useState, type FC } from "react";

import "./index.scss";
import React from "react";
import clsx from "clsx";
import { Bars3Icon } from "@heroicons/react/24/outline";

export interface NavigationProps {
initialState?: "open" | "closed";
elements: React.ReactElement[];
}

const Navigation: FC<NavigationProps> = ({
elements,
initialState = "closed",
}) => {
const navigationId = useId();

const [menuState, setMenuState] = useState<boolean>(initialState === "open");

const toggleNavigation = () => {
setMenuState((prev) => !prev);
};

return (
<nav
className={clsx(
"navigation",
menuState ? "navigation--open" : "navigation--closed",
)}
>
<button
type="button"
className="navigation__expand"
aria-label={menuState ? "Close navigation" : "Open navigation"}
aria-expanded={menuState}
aria-controls={navigationId}
onClick={toggleNavigation}
>
<Bars3Icon aria-hidden="true" focusable="false" />
</button>
<div
id={navigationId}
aria-hidden={!menuState}
className="navigation__elements"
>
{elements.map((child, idx) => (
<div key={child.key ?? idx} className="navigation__element">
{child}
</div>
))}
</div>
</nav>
);
};

export default Navigation;
40 changes: 40 additions & 0 deletions src/PageLayout/PageLayout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import type { Meta, StoryObj } from "@storybook/react-vite";

import PageLayout from ".";
import { Placeholder } from "../Placeholder";
import Header from "../Header";
import DetSysIcon from "../DetSysIcon";
import ColorSchemeToggle from "../ColorSchemeToggle";
import Navigation from "../Navigation";

const meta = {
title: "Template/PageLayout",
Expand Down Expand Up @@ -33,3 +37,39 @@ export const Default: Story = {
content: <Placeholder height="15em" label="Content" />,
},
};

export const FleshedOut: Story = {
args: {
header: (
<Header
logo={
<>
<DetSysIcon size="base" />
<h1 style={{ margin: 0 }}>DetSys</h1>
</>
}
elements={[
<ColorSchemeToggle key="color-scheme-toggle" />,
<Navigation
key="nav"
elements={[
<a key="docs" href="#">
Docs
</a>,
<a key="flakes" href="#">
Flakes
</a>,
<a key="orgs" href="#">
Orgs
</a>,
<Placeholder key="lorem" height="100%" label="Lorem" />,
<Placeholder key="ipsum" height="100%" label="Ipsum" />,
]}
/>,
]}
/>
),
content: <div style={{ height: "100em" }} />,
panes: [<Placeholder height="3em" label="Pane 1" />],
},
};
8 changes: 7 additions & 1 deletion src/PageLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ const PageLayout: FC<PageLayoutProps> = ({
{!!header && <header>{header}</header>}
<main>
<div className="page-layout__content">{content}</div>
{panes.length > 0 && <div className="page-layout__panes">{panes}</div>}
{panes.length > 0 && (
<div className="page-layout__panes">
{panes.map((pane, idx) => (
<div key={pane.key ?? idx}>{pane}</div>
))}
</div>
)}
</main>
{!!footer && <footer>{footer}</footer>}
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,5 @@ export {
} from "./LabeledTextInput";

export { type PageLayoutProps, default as PageLayout } from "./PageLayout";

export { type NavigationProps, default as Navigation } from "./Navigation";
11 changes: 11 additions & 0 deletions src/sass/_mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@
}
}

@mixin border($size) {
border-width: 1px;
border-style: solid;
border-radius: map.get(tokens.$border-radius, $size);

border-color: map.get(tokens.$border-color, dark);
@include light-mode {
border-color: map.get(tokens.$border-color, light);
}
}

@mixin transition($property, $duration) {
transition-property: $property;
transition-duration: map.get(tokens.$duration, $duration);
Expand Down