From 451388d18c636889a2ed5abee9d40418a980c175 Mon Sep 17 00:00:00 2001
From: trevoring-okta <106110543+trevoring-okta@users.noreply.github.com>
Date: Mon, 22 Jul 2024 15:25:21 -0400
Subject: [PATCH] PageTemplate and Layout component (#2219)
OKTA-721245 feat: start layout component
feat: more work on layout component
feat: documentation and syntax updates
fix: update to storybook and component
feat: add more to Storybook
fix: remove unncessary MUI ScopedCssBaseline
feat: add full-width story
feat(odyssey-react-mui): create surface component
feat(odyssey-react-mui): create stories for Grid
feat(odyssey-react-mui): remove surface styling from Grid
fix: small nit fixes
feat(odyssey-react-mui): panes => regions
fix(odyssey-react-mui): rename components and add disclaimer
fix: standardize vertical layout distance
refactor: update css
refactor: alphabetize the imports
feat: add rudimentary responsiveness to Layout
refactor: improve nested selectors
refactor: update based on code review
Merge branch 'main' into ti-OKTA-721245-layout-component
fix: update redundant type export
---
packages/odyssey-react-mui/src/Surface.tsx | 48 ++
packages/odyssey-react-mui/src/index.ts | 1 +
.../src/labs/DataComponents/DataView.tsx | 6 +-
.../labs/DataComponents/LayoutSwitcher.tsx | 10 +-
.../src/labs/DataComponents/componentTypes.ts | 6 +-
.../odyssey-react-mui/src/labs/Layout.tsx | 85 +++
.../src/labs/PageTemplate.tsx | 225 ++++++
packages/odyssey-react-mui/src/labs/index.ts | 2 +
.../components/MuiThemeDecorator.tsx | 5 +-
.../odyssey-labs/Layout/Layout.stories.tsx | 326 ++++++++
.../PageTemplate/PageTemplate.mdx | 25 +
.../PageTemplate/PageTemplate.stories.tsx | 721 ++++++++++++++++++
12 files changed, 1445 insertions(+), 15 deletions(-)
create mode 100644 packages/odyssey-react-mui/src/Surface.tsx
create mode 100644 packages/odyssey-react-mui/src/labs/Layout.tsx
create mode 100644 packages/odyssey-react-mui/src/labs/PageTemplate.tsx
create mode 100644 packages/odyssey-storybook/src/components/odyssey-labs/Layout/Layout.stories.tsx
create mode 100644 packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.mdx
create mode 100644 packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.stories.tsx
diff --git a/packages/odyssey-react-mui/src/Surface.tsx b/packages/odyssey-react-mui/src/Surface.tsx
new file mode 100644
index 0000000000..d1f3c3e4dc
--- /dev/null
+++ b/packages/odyssey-react-mui/src/Surface.tsx
@@ -0,0 +1,48 @@
+/*!
+ * Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { memo, ReactNode } from "react";
+import styled from "@emotion/styled";
+import { Paper as MuiPaper } from "@mui/material";
+
+import {
+ DesignTokens,
+ useOdysseyDesignTokens,
+} from "./OdysseyDesignTokensContext";
+
+const StyledContainer = styled(MuiPaper, {
+ shouldForwardProp: (prop) => prop !== "odysseyDesignTokens",
+})<{
+ odysseyDesignTokens: DesignTokens;
+}>(({ odysseyDesignTokens }) => ({
+ borderRadius: odysseyDesignTokens.Spacing4,
+ padding: odysseyDesignTokens.Spacing4,
+}));
+
+export type SurfaceProps = {
+ children: ReactNode;
+};
+
+const Surface = ({ children }: SurfaceProps) => {
+ const odysseyDesignTokens = useOdysseyDesignTokens();
+
+ return (
+
+ {children}
+
+ );
+};
+
+const MemoizedSurface = memo(Surface);
+MemoizedSurface.displayName = "Surface";
+
+export { MemoizedSurface as Surface };
diff --git a/packages/odyssey-react-mui/src/index.ts b/packages/odyssey-react-mui/src/index.ts
index 305d1c9692..3cd876ceb2 100644
--- a/packages/odyssey-react-mui/src/index.ts
+++ b/packages/odyssey-react-mui/src/index.ts
@@ -96,6 +96,7 @@ export * from "./ScreenReaderText";
export * from "./SearchField";
export * from "./Select";
export * from "./Status";
+export * from "./Surface";
export * from "./Tabs";
export * from "./Tag";
export * from "./TagList";
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx
index 8c5adc1c82..32b4a9389d 100644
--- a/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx
@@ -23,7 +23,7 @@ import {
densityValues,
} from "./constants";
import {
- Layout,
+ DataLayout,
UniversalProps,
ViewProps,
TableState,
@@ -47,7 +47,7 @@ import {
} from "../../OdysseyDesignTokensContext";
import styled from "@emotion/styled";
-export type DataViewProps = UniversalProps & ViewProps;
+export type DataViewProps = UniversalProps & ViewProps;
const DataViewContainer = styled("div", {
shouldForwardProp: (prop) => prop !== "odysseyDesignTokens",
@@ -100,7 +100,7 @@ const DataView = ({
const odysseyDesignTokens = useOdysseyDesignTokens();
const { t } = useTranslation();
- const [currentLayout, setCurrentLayout] = useState(
+ const [currentLayout, setCurrentLayout] = useState(
initialLayout ?? availableLayouts[0],
);
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/LayoutSwitcher.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/LayoutSwitcher.tsx
index ab5c704387..c292a977fe 100644
--- a/packages/odyssey-react-mui/src/labs/DataComponents/LayoutSwitcher.tsx
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/LayoutSwitcher.tsx
@@ -13,14 +13,14 @@
import { Dispatch, memo, useCallback, SetStateAction, useMemo } from "react";
import { useTranslation } from "react-i18next";
-import { AvailableLayouts, Layout } from "./componentTypes";
+import { AvailableLayouts, DataLayout } from "./componentTypes";
import { MenuButton } from "../../MenuButton";
import { MenuItem } from "../../MenuItem";
export type LayoutSwitcherProps = {
availableLayouts: AvailableLayouts;
- currentLayout: Layout;
- setCurrentLayout: Dispatch>;
+ currentLayout: DataLayout;
+ setCurrentLayout: Dispatch>;
};
const LayoutSwitcher = ({
@@ -31,7 +31,7 @@ const LayoutSwitcher = ({
const { t } = useTranslation();
const changeLayout = useCallback(
- (value: Layout) => {
+ (value: DataLayout) => {
setCurrentLayout(value);
},
[setCurrentLayout],
@@ -39,7 +39,7 @@ const LayoutSwitcher = ({
const memoizedMenuItems = useMemo(
() =>
- availableLayouts.map((value: Layout) => ({
+ availableLayouts.map((value: DataLayout) => ({
value,
onClick: () => changeLayout(value),
label: t(`dataview.layout.${value}`),
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts b/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts
index 13ffde63bd..eb038a9a4c 100644
--- a/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts
@@ -37,10 +37,10 @@ import { paginationTypeValues } from "../DataTablePagination";
import { ReactNode } from "react";
import { StackCardProps } from "./StackCard";
-export type Layout = (typeof availableLayouts)[number];
+export type DataLayout = (typeof availableLayouts)[number];
export type StackLayout = (typeof availableStackLayouts)[number];
-export type AvailableLayouts = Layout[];
+export type AvailableLayouts = DataLayout[];
export type AvailableStackLayouts = StackLayout[];
export type UniversalProps = {
@@ -97,7 +97,7 @@ export type StackProps = {
rowActionMenuItems?: DataTableRowActionsProps["rowActionMenuItems"];
};
-export type ViewProps = {
+export type ViewProps = {
availableLayouts?: L[];
initialLayout?: L;
stackOptions?: StackProps;
diff --git a/packages/odyssey-react-mui/src/labs/Layout.tsx b/packages/odyssey-react-mui/src/labs/Layout.tsx
new file mode 100644
index 0000000000..30ac1e1b6f
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/Layout.tsx
@@ -0,0 +1,85 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { Children, ReactNode, memo } from "react";
+import styled from "@emotion/styled";
+
+import { Box } from "../Box";
+import {
+ DesignTokens,
+ useOdysseyDesignTokens,
+} from "../OdysseyDesignTokensContext";
+
+type SupportedRegionRatios =
+ | [1]
+ | [1, 1]
+ | [1, 2]
+ | [2, 1]
+ | [1, 3]
+ | [3, 1]
+ | [1, 1, 1]
+ | [1, 1, 1, 1];
+
+export type LayoutProps = {
+ /**
+ * The supported region ratios for the Grid. Each number is a fractional unit that is mapped to the 'fr' CSS unit.
+ * e.g. [2, 1] defines a 2/3, 1/3 layout and [1, 1, 1] defines a 1/3, 1/3, 1/3 layout
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Basic_concepts_of_grid_layout#the_fr_unit
+ */
+ regions: SupportedRegionRatios;
+ /**
+ * The content of the Grid. May be a `string` or any other `ReactNode` or array of `ReactNode`s.
+ */
+ children?: ReactNode;
+};
+
+interface LayoutContentProps {
+ odysseyDesignTokens: DesignTokens;
+ regions: string;
+}
+
+const LayoutContent = styled("div", {
+ shouldForwardProp: (prop) =>
+ !["odysseyDesignTokens", "regions"].includes(prop),
+})(({ odysseyDesignTokens, regions }) => ({
+ display: "grid",
+ gridTemplateColumns: regions,
+ gridColumnGap: odysseyDesignTokens.Spacing4,
+ columnGap: odysseyDesignTokens.Spacing4,
+
+ "& + &": {
+ marginBlockStart: odysseyDesignTokens.Spacing4,
+ },
+}));
+
+const Layout = ({ regions, children }: LayoutProps) => {
+ const odysseyDesignTokens = useOdysseyDesignTokens();
+ const mappedRegions = regions
+ .map((region) => `minmax(0, ${region}fr)`)
+ .join(" ");
+
+ return (
+
+
+ {Children.toArray(children).map((child) => child)}
+
+
+ );
+};
+
+const MemoizedLayout = memo(Layout);
+MemoizedLayout.displayName = "Layout";
+
+export { MemoizedLayout as Layout };
diff --git a/packages/odyssey-react-mui/src/labs/PageTemplate.tsx b/packages/odyssey-react-mui/src/labs/PageTemplate.tsx
new file mode 100644
index 0000000000..2d3e345f77
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/PageTemplate.tsx
@@ -0,0 +1,225 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { memo, ReactElement, ReactNode } from "react";
+import styled from "@emotion/styled";
+
+import {
+ DesignTokens,
+ useOdysseyDesignTokens,
+} from "../OdysseyDesignTokensContext";
+import { DocumentationIcon } from "../icons.generated";
+import { Heading4, Subordinate } from "../Typography";
+import { Link } from "../Link";
+
+export type PageTemplateProps = {
+ /**
+ * The title of the layout to be situated in the layout header
+ */
+ title?: string;
+ /**
+ * A supplementary description to be situated in the layout header
+ */
+ description?: string;
+ /**
+ * The destination for a documentation `Link` to be situated in the layout header
+ */
+ documentationLink?: string;
+ /**
+ * The text for a documentation `Link` to be situated in the layout header
+ */
+ documentationText?: string;
+ /**
+ * An optional `Drawer` object. Can be of variant 'temporary' or 'persistent'.
+ */
+ drawer?: ReactElement;
+ /**
+ * An optional `Button` object to be situated in the layout header. Should almost always be of variant `primary`.
+ */
+ primaryCallToActionComponent?: ReactElement;
+ /**
+ * An optional `Button` object to be situated in the layout header, alongside the `callToActionPrimaryComponent`.
+ */
+ secondaryCallToActionComponent?: ReactElement;
+ /**
+ * An optional `Button` object to be situated in the layout header, alongside the other two `callToAction` components.
+ */
+ tertiaryCallToActionComponent?: ReactElement;
+ /**
+ * The content of the layout. May be a `string` or any other `ReactNode` or array of `ReactNode`s. Will often be `Grid` objects.
+ */
+ children?: ReactNode;
+ /**
+ * When set to `true`, the layout expands past its max width of 1440px and spans the entire available screen width.
+ */
+ isFullWidth?: boolean;
+};
+
+type TemplateContentProps = {
+ odysseyDesignTokens: DesignTokens;
+ isDrawerOpen?: boolean;
+ drawerVariant?: string;
+};
+
+const TemplateContainer = styled("div", {
+ shouldForwardProp: (prop) =>
+ prop !== "odysseyDesignTokens" && prop !== "isFullWidth",
+})<{
+ odysseyDesignTokens: DesignTokens;
+ isFullWidth: boolean;
+}>(({ odysseyDesignTokens, isFullWidth }) => ({
+ maxWidth: isFullWidth
+ ? "100%"
+ : `calc(1440px + ${odysseyDesignTokens.Spacing6} + ${odysseyDesignTokens.Spacing6})`,
+ marginInline: isFullWidth ? odysseyDesignTokens.Spacing6 : "auto",
+ padding: odysseyDesignTokens.Spacing6,
+}));
+
+const TemplateHeader = styled("div")(() => ({
+ display: "flex",
+ alignItems: "flex-end",
+ justifyContent: "space-between",
+}));
+
+const TemplateHeaderPrimaryContent = styled("div")(() => ({
+ [".MuiTypography-root:last-child"]: {
+ marginBlockEnd: "0",
+ },
+}));
+
+const TemplateHeaderSecondaryContent = styled("div", {
+ shouldForwardProp: (prop) => prop !== "odysseyDesignTokens",
+})<{
+ odysseyDesignTokens: DesignTokens;
+}>(({ odysseyDesignTokens }) => ({
+ alignItems: "flex-end",
+ display: "flex",
+ flexDirection: "column",
+ gap: odysseyDesignTokens.Spacing2,
+ minHeight: odysseyDesignTokens.Spacing7,
+ justifyContent: "center",
+}));
+
+const TemplateHeaderButtons = styled("div", {
+ shouldForwardProp: (prop) => prop !== "odysseyDesignTokens",
+})<{
+ odysseyDesignTokens: DesignTokens;
+}>(({ odysseyDesignTokens }) => ({
+ display: "flex",
+ gap: odysseyDesignTokens.Spacing2,
+}));
+
+const TemplateContent = styled("div", {
+ shouldForwardProp: (prop) =>
+ !["odysseyDesignTokens", "isDrawerOpen", "drawerVariant"].includes(prop),
+})(
+ ({ odysseyDesignTokens, isDrawerOpen, drawerVariant }) => ({
+ "@keyframes animate-drawer-open": {
+ "0%": {
+ gridTemplateColumns: "minmax(0, 1fr) 0",
+ },
+ "100%": {
+ gridTemplateColumns: "minmax(0, 1fr) 360px",
+ },
+ },
+ "@keyframes animate-drawer-close": {
+ "0%": {
+ gridTemplateColumns: "minmax(0, 1fr) 360px",
+ },
+ "100%": {
+ gridTemplateColumns: "minmax(0, 1fr) 0",
+ },
+ },
+ display: "grid",
+ gridGap:
+ drawerVariant === "persistent" && !isDrawerOpen
+ ? 0
+ : odysseyDesignTokens.Spacing4,
+ gap:
+ drawerVariant === "persistent" && !isDrawerOpen
+ ? 0
+ : odysseyDesignTokens.Spacing4,
+ marginBlock: odysseyDesignTokens.Spacing4,
+ gridTemplateColumns:
+ drawerVariant === "persistent"
+ ? isDrawerOpen
+ ? "minmax(0, 1fr) 360px"
+ : "minmax(0, 1fr) 0"
+ : "minmax(0, 1fr)",
+ animation:
+ drawerVariant === "persistent" && isDrawerOpen
+ ? "animate-drawer-open 225ms cubic-bezier(0, 0, 0.2, 1)"
+ : "animate-drawer-close 225ms cubic-bezier(0, 0, 0.2, 1)",
+ }),
+);
+
+const PageTemplate = ({
+ title,
+ description,
+ documentationLink,
+ documentationText,
+ primaryCallToActionComponent,
+ secondaryCallToActionComponent,
+ tertiaryCallToActionComponent,
+ children,
+ drawer,
+ isFullWidth = false,
+}: PageTemplateProps) => {
+ const odysseyDesignTokens = useOdysseyDesignTokens();
+ const { isOpen: isDrawerOpen, variant: drawerVariant } = drawer?.props ?? {};
+
+ return (
+
+
+
+ {title && {title}}
+ {description && {description}}
+
+
+
+ {documentationLink && (
+ }>
+ {documentationText}
+
+ )}
+ {(primaryCallToActionComponent ||
+ secondaryCallToActionComponent ||
+ tertiaryCallToActionComponent) && (
+
+ {tertiaryCallToActionComponent}
+ {secondaryCallToActionComponent}
+ {primaryCallToActionComponent}
+
+ )}
+
+
+
+ {children}
+ {drawer}
+
+
+ );
+};
+
+const MemoizedPageTemplate = memo(PageTemplate);
+MemoizedPageTemplate.displayName = "PageTemplate";
+
+export { MemoizedPageTemplate as PageTemplate };
diff --git a/packages/odyssey-react-mui/src/labs/index.ts b/packages/odyssey-react-mui/src/labs/index.ts
index c6b2208783..5101a6c3d7 100644
--- a/packages/odyssey-react-mui/src/labs/index.ts
+++ b/packages/odyssey-react-mui/src/labs/index.ts
@@ -23,9 +23,11 @@ export * from "./DataComponents";
export * from "./DataTablePagination";
export * from "./DataFilters";
export * from "./FileUpload";
+export * from "./Layout";
export * from "./materialReactTableTypes";
/** @deprecated Will be removed in a future Odyssey version in lieu of DataTable */
export * from "./StaticTable";
+export * from "./PageTemplate";
/** @deprecated Will be removed in a future Odyssey version in lieu of DataTable */
export * from "./PaginatedTable";
diff --git a/packages/odyssey-storybook/.storybook/components/MuiThemeDecorator.tsx b/packages/odyssey-storybook/.storybook/components/MuiThemeDecorator.tsx
index 9877161ef1..0afdafb0db 100644
--- a/packages/odyssey-storybook/.storybook/components/MuiThemeDecorator.tsx
+++ b/packages/odyssey-storybook/.storybook/components/MuiThemeDecorator.tsx
@@ -3,7 +3,6 @@ import {
CssBaseline,
OdysseyProvider,
} from "@okta/odyssey-react-mui";
-import { ScopedCssBaseline } from "@mui/material";
import { ThemeProvider as StorybookThemeProvider } from "@storybook/theming";
import type { Decorator } from "@storybook/react";
import * as odysseyTokens from "@okta/odyssey-design-tokens";
@@ -25,9 +24,7 @@ export const MuiThemeDecorator: Decorator = (Story, context) => {
-
-
-
+
diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/Layout/Layout.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/Layout/Layout.stories.tsx
new file mode 100644
index 0000000000..ca7a2e839b
--- /dev/null
+++ b/packages/odyssey-storybook/src/components/odyssey-labs/Layout/Layout.stories.tsx
@@ -0,0 +1,326 @@
+/*!
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { Meta, StoryObj } from "@storybook/react";
+import styled from "@emotion/styled";
+
+import { Layout, LayoutProps } from "@okta/odyssey-react-mui/labs";
+import { Subordinate } from "@okta/odyssey-react-mui";
+import { MuiThemeDecorator } from "../../../../.storybook/components";
+// import { Surface } from "@okta/odyssey-react-mui";
+
+const VisibleRegion = styled.div({
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ padding: "16px",
+ border: "1px dashed #cbcbcb",
+});
+
+const RegionLabel = styled.h3({
+ margin: 0,
+});
+
+const DisclaimerContainer = styled.div({
+ maxWidth: "55ch",
+ marginBlockEnd: "8px",
+});
+
+const RegionDisclaimer = () => {
+ return (
+
+
+ NOTE: Dashed border and padding are applied to show
+ region boundries in Storybook only. They will not be present when you
+ are using Layout
+
+
+ );
+};
+
+const storybookMeta: Meta = {
+ title: "Labs Components/Layout",
+ component: Layout,
+ argTypes: {
+ regions: {
+ control: "text",
+ description:
+ "The supported region ratios for the `Layout`. Each number is a fractional unit that is mapped to the 'fr' CSS unit. For example: [2, 1] defines a 2/3, 1/3 layout and [1, 1, 1] defines a 1/3, 1/3, 1/3 layout",
+ table: {
+ type: {
+ summary: "SupportedRegionRatios",
+ },
+ },
+ },
+ children: {
+ control: null,
+ description:
+ "The content of the `Layout`. May be a `string` or any other `ReactNode` or array of `ReactNode`s. Will often be either a single `Surface` or multiple `Surface's",
+ table: {
+ type: {
+ summary: "ReactNode",
+ },
+ },
+ },
+ },
+ decorators: [
+ (Story) => {
+ return (
+ <>
+
+
+ >
+ );
+ },
+ MuiThemeDecorator,
+ ],
+ parameters: {
+ backgrounds: {
+ default: "gray",
+ values: [
+ { name: "gray", value: "#f4f4f4" },
+ { name: "white", value: "#ffffff" },
+ ],
+ },
+ },
+};
+
+export default storybookMeta;
+
+export const Single: StoryObj = {
+ args: {
+ regions: [1],
+ },
+ render: function C(args) {
+ return (
+
+
+ Region
+
+
+ );
+ },
+};
+
+export const Split: StoryObj = {
+ args: {
+ regions: [1, 1],
+ },
+ render: function C(args) {
+ return (
+
+
+ Region
+
+
+ Region
+
+
+ );
+ },
+};
+
+export const TwoThirdsStart: StoryObj = {
+ args: {
+ regions: [2, 1],
+ },
+ render: function C(args) {
+ return (
+
+
+ Region
+
+
+ Region
+
+
+ );
+ },
+};
+
+export const TwoThirdsEnd: StoryObj = {
+ args: {
+ regions: [1, 2],
+ },
+ render: function C(args) {
+ return (
+
+
+ Region
+
+
+ Region
+
+
+ );
+ },
+};
+
+export const ThreeFourthsStart: StoryObj = {
+ args: {
+ regions: [3, 1],
+ },
+ render: function C(args) {
+ return (
+
+
+ Region
+
+
+ Region
+
+
+ );
+ },
+};
+
+export const ThreeFourthsEnd: StoryObj = {
+ args: {
+ regions: [1, 3],
+ },
+ render: function C(args) {
+ return (
+
+
+ Region
+
+
+ Region
+
+
+ );
+ },
+};
+
+export const ThreeRegionSplit: StoryObj = {
+ args: {
+ regions: [1, 1, 1],
+ },
+ render: function C(args) {
+ return (
+
+
+ Region
+
+
+ Region
+
+
+ Region
+
+
+ );
+ },
+};
+
+export const FourRegionSplit: StoryObj = {
+ args: {
+ regions: [1, 1, 1, 1],
+ },
+ render: function C(args) {
+ return (
+
+
+ Region
+
+
+ Region
+
+
+ Region
+
+
+ Region
+
+
+ );
+ },
+};
+
+export const KitchenSink: StoryObj = {
+ render: function () {
+ return (
+ <>
+
+
+ Region
+
+
+
+
+ Region
+
+
+ Region
+
+
+
+
+ Region
+
+
+ Region
+
+
+
+
+ Region
+
+
+ Region
+
+
+
+
+ Region
+
+
+ Region
+
+
+
+
+ Region
+
+
+ Region
+
+
+
+
+ Region
+
+
+ Region
+
+
+ Region
+
+
+
+
+ Region
+
+
+ Region
+
+
+ Region
+
+
+ Region
+
+
+ >
+ );
+ },
+};
diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.mdx b/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.mdx
new file mode 100644
index 0000000000..31b98445e2
--- /dev/null
+++ b/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.mdx
@@ -0,0 +1,25 @@
+import {
+ Canvas,
+ Meta,
+ Title,
+ Subtitle,
+ Description,
+ Primary,
+ Controls,
+ Stories,
+} from "@storybook/addon-docs";
+import { Story } from "@storybook/blocks";
+import * as PageTemplateStories from "./PageTemplate.stories";
+
+
+
+
+
+
+
+`PageTemplate` is a multi-purpose container that enforces Odyssey layout conventions, ensuring that
+your screens look consistent without you doing extra work.
+
+If you're an Okta employee, you can see the [Figma here](https://www.figma.com/design/jMsVlc6QrA5dQXqPEEVT7S/%F0%9F%93%A6-Odyssey-Templates?node-id=1862-40643).
+
+
diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.stories.tsx
new file mode 100644
index 0000000000..d05a52415a
--- /dev/null
+++ b/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.stories.tsx
@@ -0,0 +1,721 @@
+/*!
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { Meta, StoryObj } from "@storybook/react";
+
+import { MuiThemeDecorator } from "../../../../.storybook/components";
+import {
+ Person,
+ columns as personColumns,
+ data as personData,
+} from "../../odyssey-mui/DataTable/personData";
+import {
+ Layout,
+ PageTemplate,
+ PageTemplateProps,
+} from "@okta/odyssey-react-mui/labs";
+import {
+ Box,
+ Button,
+ Checkbox,
+ DataTable,
+ Dialog,
+ Drawer,
+ Form as OdysseyForm,
+ Heading5,
+ MenuButton,
+ MenuItem,
+ Paragraph,
+ Support,
+ Surface,
+ TextField,
+ useOdysseyDesignTokens,
+} from "@okta/odyssey-react-mui";
+import { useCallback, useState } from "react";
+
+const drawerLongText = (
+ <>
+
+ Okta Privileged Access is a Privileged Access Management (PAM) solution
+ designed to help customers mitigate the risk of unauthorized access to
+ resources, a critical area of security and risk management in any
+ organization. Okta Privileged Access builds on the current server access
+ control capabilities provided with Okta Advanced Server Access and
+ delivers a unified approach to managing access to all your privileged
+ accounts. It securely connects people, machines, and applications to
+ privileged resources such as servers, containers, and enterprise apps.
+
+
+ A critical capability that Okta Privileged Access offers is the separation
+ of administrative roles and responsibilities. Management of users and
+ groups, resources, and security are separated, with each administrative
+ role designed to perform a specific function. For example, the management
+ of security policies to access resources is separated and decoupled from
+ the administration of the resources. To meet this requirement, the team
+ that sets the policy is separated from the team that administers the
+ resource. Likewise, the administrator managing users and groups can only
+ perform user and group management tasks and isn't involved in
+ administering resources or creating security policies.
+
+
+ The level of access within a Okta Privileged Access team depends on the
+ role that you're assigned and the permissions granted to that role. The
+ table below discusses the types of roles, and each has a unique set of
+ permissions and restrictions.To start using Okta Privileged Access, you
+ need to add the Okta Privileged Access OIN application to your Okta org.
+ You can then sync your users and groups from the Okta Universal Directory
+ by configuring SCIM. End users must install the Okta Privileged Access
+ client in their local machine, enroll the client, and then access their
+ dashboard using the link provided by their team administrator.
+
+ >
+);
+
+const drawerShortText = (
+
+ Okta Privileged Access is a Privileged Access Management (PAM) solution
+ designed to help customers mitigate the risk of unauthorized access to
+ resources, a critical area of security and risk management in any
+ organization. Okta Privileged Access builds on the current server access
+ control capabilities provided with Okta Advanced Server Access and delivers
+ a unified approach to managing access to all your privileged accounts. It
+ securely connects people, machines, and applications to privileged resources
+ such as servers, containers, and enterprise apps.
+
+);
+
+const storybookMeta: Meta = {
+ title: "Labs Components/PageTemplate",
+ component: PageTemplate,
+ argTypes: {
+ title: {
+ control: "text",
+ description: "The title to be situated in the `PageTemplate` header",
+ table: {
+ type: {
+ summary: "string",
+ },
+ },
+ },
+ description: {
+ control: "text",
+ description:
+ "A supplementary description to be situated in the `PageTemplate` header",
+ table: {
+ type: {
+ summary: "string",
+ },
+ },
+ },
+ documentationLink: {
+ control: "text",
+ description:
+ "The destination for a documentation `Link` to be situated in the `PageTemplate` header",
+ table: {
+ type: {
+ summary: "string",
+ },
+ },
+ },
+ documentationText: {
+ control: "text",
+ description:
+ "The text for a documentation `Link` to be situated in the `PageTemplate` header",
+ table: {
+ type: {
+ summary: "string",
+ },
+ },
+ },
+ drawer: {
+ control: null,
+ description:
+ "An optional `Drawer` object. Can be of variant 'temporary' or 'persistent'.",
+ table: {
+ type: {
+ summary: "ReactElement",
+ },
+ },
+ },
+ primaryCallToActionComponent: {
+ control: null,
+ description:
+ "An optional `Button` object to be situated in the `PageTemplate` header. Should almost always be of variant `primary`.",
+ table: {
+ type: {
+ summary: "ReactElement",
+ },
+ },
+ },
+ secondaryCallToActionComponent: {
+ control: null,
+ description:
+ "An optional `Button` object to be situated in the `PageTemplate` header, alongside the `callToActionPrimaryComponent`.",
+ table: {
+ type: {
+ summary: "ReactElement",
+ },
+ },
+ },
+ tertiaryCallToActionComponent: {
+ control: null,
+ description:
+ "An optional `Button` object to be situated in the `PageTemplate` header, alongside the other two `callToAction` components.",
+ table: {
+ type: {
+ summary: "ReactElement",
+ },
+ },
+ },
+ children: {
+ control: null,
+ description:
+ "The content of the `PageTemplate`. May be a `string` or any other `ReactNode` or array of `ReactNode`s. Will often be `Grid` objects.",
+ table: {
+ type: {
+ summary: "ReactNode",
+ },
+ },
+ },
+ isFullWidth: {
+ control: "boolean",
+ description:
+ "When set to `true`, the `PageTemplate` expands past its max width of 1440px and spans the entire available screen width.",
+ table: {
+ type: {
+ summary: "boolean",
+ },
+ defaultValue: {
+ summary: false,
+ },
+ },
+ },
+ },
+ decorators: [MuiThemeDecorator],
+ parameters: {
+ backgrounds: {
+ default: "gray",
+ values: [
+ { name: "gray", value: "#f4f4f4" },
+ { name: "white", value: "#ffffff" },
+ ],
+ },
+ },
+};
+
+export default storybookMeta;
+
+export const KitchenSink: StoryObj = {
+ args: {
+ title: "Table title",
+ description: "Optional brief description about the page",
+ documentationLink: "https://www.okta.com",
+ documentationText: "Documentation",
+ isFullWidth: false,
+ },
+ render: function C(args) {
+ const [data] = useState(personData.slice(0, 10));
+ const [isOverlayDrawerVisible, setIsOverlayVisible] = useState(false);
+ const [isDialogVisible, setIsDialogVisible] = useState(false);
+
+ const getData = useCallback(() => {
+ return data;
+ }, [data]);
+
+ const onOpenOverlayDrawer = useCallback(() => {
+ setIsOverlayVisible(true);
+ }, []);
+
+ const onCloseOverlayDrawer = useCallback(() => {
+ setIsOverlayVisible(false);
+ }, []);
+
+ const onOpenDialog = useCallback(() => {
+ setIsDialogVisible(true);
+ }, []);
+
+ const onCloseDialog = useCallback(() => {
+ setIsDialogVisible(false);
+ }, []);
+
+ return (
+
+ }
+ secondaryCallToActionComponent={
+
+ }
+ tertiaryCallToActionComponent={
+ View details,
+ ,
+ ,
+ ]}
+ />
+ }
+ drawer={
+
+ }
+ onClose={onCloseOverlayDrawer}
+ isOpen={isOverlayDrawerVisible}
+ showDividers
+ >
+ {drawerLongText}
+
+ }
+ isFullWidth={args.isFullWidth}
+ >
+
+ }
+ secondaryCallToActionComponent={
+
+ }
+ onClose={onCloseDialog}
+ isOpen={isDialogVisible}
+ />
+
+
+
+
+
+