Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add page layout, header, panel #56

Merged
merged 1 commit into from
Dec 5, 2024
Merged
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
2 changes: 1 addition & 1 deletion .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const preview: Preview = {
parameters: {
layout: "centered",
backgrounds: {
default: "light-background",
default: "dark-background",
values: [
{
name: "dark-background",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const Nested: Story = {
return (
<>
<Story />
<Modal id="nested" title="Nested modal" size="lg" hasBackdrop>
<Modal id="nested" title="Nested modal" size="md" hasBackdrop>
{({ closeModal }) => (
<ModalLayout.CallToAction
title="A nested modal example"
Expand Down
44 changes: 18 additions & 26 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import {
DialogTitle,
} from "@headlessui/react";
import { useModalContext } from "./ModalContext";
import { classNames, variantClassNames } from "../../util/classes";
import { classNames } from "../../util/classes";
import { InkIcon } from "../..";
import { useEffect, useRef } from "react";
import { InkHeader } from "../../layout/InkParts";
import { InkPanel } from "../../layout/InkParts/InkPanel";

export interface ModalProps<TOnCloseProps = boolean> {
id: string;
Expand Down Expand Up @@ -69,32 +71,22 @@ export const Modal = <TOnCloseProps,>({
"ink:flex ink:items-center ink:justify-center"
)}
>
<DialogPanel
transition
className={classNames(
"ink:flex ink:flex-col ink:justify-between ink:gap-3 ink:p-3",
"ink:bg-background-light ink:shadow-modal ink:rounded-lg",
"ink:transition-default-animation ink:data-closed:scale-95 ink:data-closed:opacity-0",
variantClassNames(size, {
lg: "ink:min-w-[320px] ink:sm:min-w-[640px] ink:min-h-[480px] ink:max-w-4xl",
md: "ink:min-w-[200px] ink:sm:min-w-[300px] ink:min-h-[300px]",
})
)}
>
<DialogTitle
className={classNames(
"ink:w-full ink:flex ink:items-center ink:justify-between"
)}
>
<div className="ink:text-h4">{title}</div>
<InkIcon.Close
className="ink:cursor-pointer ink:size-3"
onClick={() => handleClose()}
<DialogPanel transition>
<InkPanel size={size} centered>
<DialogTitle
as={InkHeader}
title={title}
icon={
<InkIcon.Close
className="ink:cursor-pointer ink:size-3"
onClick={() => handleClose()}
/>
}
/>
</DialogTitle>
<div className="ink:flex-1 ink:flex ink:flex-col ink:justify-center ink:items-center">
{children({ closeModal: handleClose })}
</div>
<div className="ink:flex-1 ink:flex ink:flex-col ink:justify-center ink:items-center">
{children({ closeModal: handleClose })}
</div>
</InkPanel>
</DialogPanel>
</div>
</Dialog>
Expand Down
34 changes: 34 additions & 0 deletions src/layout/ForStories/ExampleDynamicContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { InkIcon } from "../..";
import { InkHeader } from "../InkParts";
import { InkPanel } from "../InkParts/InkPanel";

const ExamplePanel = ({
className,
text,
}: {
className?: string;
text: string;
}) => (
<InkPanel className={className}>
<InkHeader title={text} icon={<InkIcon.Settings />} />
<div className="ink:flex-1">{text}</div>
</InkPanel>
);

export const ExampleDynamicContent = ({
className,
columns,
}: {
className?: string;
columns?: number;
}) => {
if (!columns || columns === 1)
return <ExamplePanel className={className} text="Only Content" />;
return (
<>
<ExamplePanel className={className} text="Column 1" />
{columns > 1 && <ExamplePanel className={className} text="Column 2" />}
{columns > 2 && <ExamplePanel className={className} text="Column 3" />}
</>
);
};
23 changes: 23 additions & 0 deletions src/layout/ForStories/ExampleSideNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { InkIcon } from "../..";
import { InkLayoutSideNav } from "../InkLayout";

export const ExampleSideNav = () => {
return (
<InkLayoutSideNav
links={[
{
children: "Home",
href: "#home",
icon: <InkIcon.Home />,
target: "_self",
},
{
children: "Settings",
href: "#settings",
icon: <InkIcon.Settings />,
target: "_self",
},
]}
/>
);
};
13 changes: 13 additions & 0 deletions src/layout/ForStories/ExampleTopNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { SegmentedControl } from "../../components";

export const ExampleTopNav = () => {
return (
<SegmentedControl
options={[
{ children: "Home", value: "home", selectedByDefault: true },
{ children: "Settings", value: "settings" },
]}
onOptionChange={() => {}}
/>
);
};
71 changes: 28 additions & 43 deletions src/layout/InkLayout/InkLayout.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react";
import { InkIcon, SegmentedControl } from "../..";
import { InkIcon } from "../..";
import { InkLayout, InkLayoutProps, InkLayoutSideNav } from "./index";
import { InkPageLayout } from "../InkParts/InkPageLayout";
import { ExampleSideNav } from "../ForStories/ExampleSideNav";
import { ExampleTopNav } from "../ForStories/ExampleTopNav";
import { InkPanel } from "../InkParts/InkPanel";

const SideNav = () => {
return (
<InkLayoutSideNav
links={[
{
children: "Home",
href: "#home",
icon: <InkIcon.Home />,
target: "_self",
},
{
children: "Settings",
href: "#settings",
icon: <InkIcon.Settings />,
target: "_self",
},
]}
/>
);
};

const TopNav = () => {
return (
<SegmentedControl
options={[
{ children: "Home", value: "home", selectedByDefault: true },
{ children: "Settings", value: "settings" },
]}
onOptionChange={() => {}}
/>
);
};

/**
* This layout component provides a unified layout that can be used for most pages.
* <br/>
* It's content is defined by the children prop, which can be used with the [InkPageLayout component](?path=/docs/layouts-inkpagelayout--docs)
*/
const meta: Meta<InkLayoutProps> = {
title: "Layouts/InkLayout",
component: InkLayout,
Expand All @@ -43,10 +19,16 @@ const meta: Meta<InkLayoutProps> = {
},
tags: ["autodocs"],
args: {
children: <div>Some content</div>,
children: (
<InkPageLayout>
<InkPanel>
<div>Some content</div>
</InkPanel>
</InkPageLayout>
),
headerContent: <div>Header content</div>,
topNavigation: <TopNav />,
sideNavigation: <SideNav />,
topNavigation: <ExampleTopNav />,
sideNavigation: <ExampleSideNav />,
},
};

Expand All @@ -57,8 +39,9 @@ export const Simple: Story = {
args: {},
};

// Serves as a fun example of how to use `linkAs` to customize the underlying component of `InkNavLink`.
// It is necessary to allow users to pass `Link`
/**
* The side nav can be a custom component for routing, for instance, if you want to use NextJS' own {`<Link />`} component.
*/
export const SideNavWithCustomButtons: Story = {
args: {
sideNavigation: (
Expand All @@ -82,10 +65,12 @@ export const SideNavWithCustomButtons: Story = {
/>
),
children: (
<div>
The side nav can be a custom component for routing, for instance, if you
want to use NextJS' own {`<Link />`} component.
</div>
<InkPageLayout>
<InkPanel>
The side nav can be a custom component for routing, for instance, if
you want to use NextJS' own {`<Link />`} component.
</InkPanel>
</InkPageLayout>
),
},
};
10 changes: 4 additions & 6 deletions src/layout/InkLayout/InkLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ export const InkLayout: React.FC<InkLayoutProps> = ({
return (
<div
className={classNames(
"ink:flex ink:flex-col ink:min-h-screen ink:min-w-[320px] ink:font-default ink:text-text-default ink:gap-5"
"ink:flex ink:flex-col ink:min-h-full ink:pb-5 ink:min-w-[320px] ink:font-default ink:text-text-default ink:gap-5 ink:box-border"
)}
>
<div className="ink:w-full ink:flex ink:justify-between ink:items-center ink:gap-3 ink:px-5 ink:pt-4">
<div className="ink:w-full ink:flex ink:justify-between ink:items-center ink:gap-3 ink:px-5 ink:pt-4 ink:box-border">
<div className="ink:flex ink:items-center ink:justify-start ink:size-6 ink:gap-2">
{mainIcon}
</div>
Expand All @@ -31,15 +31,13 @@ export const InkLayout: React.FC<InkLayoutProps> = ({
<div className="ink:flex ink:items-center">{headerContent}</div>
) : null}
</div>
<div className="ink:flex ink:flex-1">
<div className="ink:flex ink:flex-1 ink:mr-5 ink:box-border">
{sideNavigation && (
<div className={classNames("ink:w-[260px] ink:px-4")}>
{sideNavigation}
</div>
)}
<div className="ink:flex-1 ink:bg-background-light ink:rounded-lg ink:shadow-layout ink:p-3 ink:mr-5">
{children}
</div>
{children}
</div>
</div>
);
Expand Down
35 changes: 35 additions & 0 deletions src/layout/InkParts/InkHeader.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Meta, StoryObj } from "@storybook/react";
import { InkHeader, InkHeaderProps } from "./InkHeader";
import { InkIcon } from "../..";

/**
* This component provides a unified header that can be used at the top of a page or a modal.
*/
const meta: Meta<InkHeaderProps> = {
decorators: [
(Story) => (
<div className="ink:w-full ink:p-3 ink:bg-background-container ink:rounded-lg">
<Story />
</div>
),
],
title: "Layouts/InkHeader",
component: InkHeader,
tags: ["autodocs"],
args: {
title: "Example Title",
},
};

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

export const Simple: Story = {
args: {},
};

export const WithIcon: Story = {
args: {
icon: <InkIcon.Home />,
},
};
26 changes: 26 additions & 0 deletions src/layout/InkParts/InkHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { PropsWithChildren } from "react";
import { classNames } from "../../util/classes";

export interface InkHeaderProps extends PropsWithChildren {
title: React.ReactNode;
children?: React.ReactNode;
icon?: React.ReactNode;
}

export const InkHeader: React.FC<InkHeaderProps> = ({
title,
icon,
children,
}) => {
return (
<div
className={classNames(
"ink:w-full ink:flex ink:items-center ink:justify-between ink:font-default ink:box-border ink:gap-2 ink:text-text-default"
)}
>
<div className="ink:text-h4 ink:whitespace-nowrap">{title}</div>
{children}
<div className="ink:size-3 ink:shrink-0">{icon}</div>
</div>
);
};
49 changes: 49 additions & 0 deletions src/layout/InkParts/InkPageLayout.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Meta, StoryObj } from "@storybook/react";
import { InkPageLayout, InkPageLayoutProps } from "./InkPageLayout";
import { InkLayout } from "../InkLayout";
import { ExampleDynamicContent } from "../ForStories/ExampleDynamicContent";

/**
* This component provides a column layout for a page.
* The `columns` prop determines the number of columns to display. You must then pass the same number of children to the component.
* <br/>
* Note that the `InkLayout` component is included only as an example. It is not required for this component to function.
*/
const meta: Meta<InkPageLayoutProps> = {
parameters: {
layout: "fullscreen",
},
decorators: [
(Story, { args }) => (
<InkLayout
sideNavigation={<div>Side Navigation</div>}
headerContent={<div>Header Content</div>}
>
<Story
args={{
...args,
children: args.children ?? (
<ExampleDynamicContent
columns={args.columns}
className="ink:min-h-[500px]"
/>
),
}}
/>
</InkLayout>
),
],
title: "Layouts/InkPageLayout",
component: InkPageLayout,
tags: ["autodocs"],
args: {
columns: 1,
},
};

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

export const Simple: Story = {
args: {},
};
Loading
Loading